Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
241 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
The identity bug. AKA The not-equal bug. | ||
|
||
# The Bug. | ||
|
||
When you change a property of an object in the shared object, it is often (always?) replaced with another object with the same values. | ||
|
||
this seems like a kind of big deal problem... | ||
|
||
```javascript | ||
const s1 = shared.sprites[0]; | ||
const s2 = shared.sprites[0]; | ||
|
||
console.log("match before", s1 === shared.sprites[0], s1 === s2); // true, true | ||
console.log(s1.touch++); | ||
const s3 = shared.sprites[0]; | ||
console.log("match after", s1 === s3, s1 === s2); // false, true | ||
``` | ||
|
||
# Why? | ||
|
||
Changing a property on s1 triggers the following: | ||
|
||
1. on-change observes the change, tells p5.party | ||
2. p5.party tells local deepstream client | ||
3. deepstreem client updates the data locally (and starts async remote broadcast process) | ||
4. deepstream client tells p5.party (local) data has changed | ||
5. p5.party overwrites the shared object's properties with the new values from deepstreem | ||
|
||
At this point, the first item in the array (and maybe every thing in shared!) is a different object than before (with the same values). The values in the objects match, but they are different objects. === returns false. | ||
|
||
# Multiple copies of the data to keep in sync | ||
|
||
client app -reads/writes-> onchange's proxy -wraps-> p5.party's Record.shared -syncsto-> deepstream client data -syncsto-> deepstream server | ||
|
||
# Does changing properties on s1 change s3 also? | ||
|
||
No! | ||
|
||
```javascript | ||
s1.check = random(); | ||
s3.check = random(); | ||
console.log(s1.check === s3.check); // false | ||
``` | ||
|
||
s1 and s2 are references to the same object, both set before any changes to shared data are made. | ||
|
||
s3 is created after the data is changed, and so after the objects are replaced with clones. | ||
|
||
s1 points to the original value, s3 points to a clone | ||
|
||
# Two proxies with two different underlying objects. | ||
|
||
This isn't just two different proxy objects being made of the same data. There are two data objects, and each has a proxy. | ||
|
||
# How does any of this work then? | ||
|
||
```javascript | ||
const s1 = shared.sprites[0]; // get a reference to shared data | ||
s1.animal = "bat"; // mutate shared data | ||
// change is noted and synced | ||
// shared.sprites[0] is replaced by new clone with new data. s1 is now an "old" clone | ||
// s1.animal doesn't get updated, but doesn't NEED to be in the SIMPLEST cases because s1 was updated by the actual assignment before the sync even happened | ||
s1.name = "barry"; // mutate shared data | ||
// change is noted and synced: even though s1 is and old clone, it still sends changes just fine (but won't receive them) | ||
``` | ||
|
||
# Possible solutions? | ||
|
||
maybe `_onServerChangedData(data)` can be changed to very carefully update only specific changes? why don't we get more detailed info to act on from deepstream anyway? some kind of "deep assign?" | ||
|
||
https://github.com/n1ru4l/graphql-live-query/tree/main/packages/json-patch-plus | ||
https://github.com/Two-Screen/symmetry/ | ||
https://github.com/AsyncBanana/microdiff | ||
https://news.ycombinator.com/item?id=29130661 | ||
|
||
# Second Try | ||
|
||
This was the second version, using the symmetry package to generate and apply a patch. to get symmetry to work would take changing its internals to patch-in-place and ultimately it looked like a hand rolled solution would be better. | ||
|
||
```javascript | ||
console.log("this: shared", this.#shared); | ||
const p = createPatch(this.#shared, data); | ||
if (p === "none") return; | ||
if (p === "reset") | ||
log.error( | ||
"Patch is full reset, reset patches not supported", | ||
this.#shared, | ||
data | ||
); | ||
if (typeof p === "object") applyPatch(this.#shared, p, true); | ||
``` | ||
|
||
# Original | ||
|
||
This was the original code for updating Record.#shared. It wipes out everying in #shared and then copies | ||
everything from data in. This replaced even unchanged things even unchanged things with duplicates. This worked surprisingly well! We built all the initial demos on this. But leads to the idenity bug. | ||
|
||
```javascript | ||
for (const key in this.#shared) { | ||
delete this.#shared[key]; | ||
} | ||
for (const key in data) { | ||
this.#shared[key] = data[key]; | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
I considred using the symmetry npm package to patch Record.#shared with incoming data. | ||
After some experimenting, I switched creating a custom function to patch in place. | ||
These notes are thoughts on what changes would have been needed to get symmetry to work for this use case. | ||
|
||
# patch in place | ||
|
||
rather than creating a new object from input and patch, change input by applying patch. | ||
|
||
# document patch format | ||
|
||
explain the format and vocabulary (t, a, o, r, s, p) in the patch format | ||
|
||
# reset patches | ||
|
||
comparing unrelated objects returns "reset" | ||
i'd like it to return a patch that effects the reset | ||
|
||
```javascript | ||
const input = { a: 1 }; | ||
const input = { b: 2 }; | ||
createPatch(input, output); // "reset" | ||
``` | ||
|
||
// adding a common key/value to both objects achieves this | ||
|
||
```javascript | ||
const input = { forcePatch: true, a: 1 }; | ||
const input = { forcePatch: true, b: 2 }; | ||
createPatch(input, output); // {"t":"o","r":["a"],"s":{"b":2}} | ||
``` | ||
|
||
# no-op patches | ||
|
||
comparing two identical objects returns "none" | ||
I'd like it to return a no-op patch | ||
|
||
```javascript | ||
const input = { a: 1 }; | ||
const output = { a: 1 }; | ||
createPatch(input, output); // "none" | ||
``` | ||
|
||
prefer somethig like `{"t": "o"}` that has no r, s, or p | ||
|
||
// started going down the route of adding a forcePatch flag to create Patch | ||
// for this to work it would have to work recursively, so i'd need to | ||
// modify symetry's createPatch (like i did ofr apply patch below) | ||
// before I do that, i should look to see if a "reset" patch would ever actually | ||
// happen in this specific use case | ||
// export function createPatch(left, right, forcePatch = false) { | ||
// if (forcePatch) { | ||
// left.\_forcePatch = true; | ||
// } | ||
// console.log("cp", left, right); | ||
// const p = \_createPatch(left, right); | ||
// return p; | ||
// } | ||
|
||
#shared has a [Symbol.for("Record")] key that points back at the owning record. it is being considerd by symmetery, is that a problem? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters