Skip to content

Commit

Permalink
A Store based on LocalStorage and lots of Doc updates
Browse files Browse the repository at this point in the history
  • Loading branch information
Matthew Forrester committed Jun 9, 2013
1 parent c0b63e3 commit 65d879c
Show file tree
Hide file tree
Showing 18 changed files with 298 additions and 132 deletions.
2 changes: 1 addition & 1 deletion README.apply-after.dot
Expand Up @@ -2,7 +2,7 @@ digraph structs {
node [shape=record];
subgraph clusterqueue {
label = "Queue";
structqueue [shape=record,label="{ Positon | | 0 | 1 | 2 | 3 } | { Dataset | <f1> | Car | Person | Car | <f2> Car }|{ Datakey | | Ford | Simon | Ford | Subaru }|{ Version | | 2 | 6 | 3 | 4 }|{ Operation | | Update | Set | Remove | Update }|{ Update | <f3> | \{...\} | \{...\} | \{...\} | <f4> \{ \"$inc \{ \"Likes\": 1 \}\} }"];
structqueue [shape=record,label="{ Positon | | 0 | 1 | 2 | 3 } | { Dataset | <f1> | Car | Person | Car | <f2> Car }|{ Datakey | | Ford | Simon | Ford | Subaru }|{ Operation | | Update | Set | Remove | Update }|{ Update | <f3> | \{...\} | \{...\} | \{...\} | <f4> \{ \"$inc \{ \"Likes\": 1 \}\} }"];
}
subgraph clusterstore {
label = "Store";
Expand Down
2 changes: 1 addition & 1 deletion README.download_updates_from_server.dot
Expand Up @@ -15,7 +15,7 @@ digraph a {
JLib -> Server [style=dashed, label="XHR? (2 & 3)"];
Server -> JLib [style=dashed];
JLib -> JLibCallback [label="(4)"];
JLibCallback -> SyncIt [fontcolor=white, color=white];
JLibCallback -> SyncIt [label="syncIt.feed()", fontcolor=white, color=white];
SyncIt -> SyncItCallback [fontcolor=white, color=white];
SyncIt -> SyncItEventHandler [fontcolor=white, color=white];
SyncIt -> SyncItEventHandler [fontcolor=white, color=white];
Expand Down
2 changes: 1 addition & 1 deletion README.feed_into_syncit.dot
Expand Up @@ -15,7 +15,7 @@ digraph a {
JLib -> Server [fontcolor=white, color=white, label="XHR? (2 & 3)"];
Server -> JLib [fontcolor=white, color=white];
JLib -> JLibCallback [fontcolor=white, color=white];
JLibCallback -> SyncIt;
JLibCallback -> SyncIt [label="syncIt.feed()"];
SyncIt -> SyncItCallback;
SyncIt -> SyncItEventHandler;
SyncIt -> SyncItEventHandler;
Expand Down
11 changes: 11 additions & 0 deletions README.getfirst.dot
@@ -0,0 +1,11 @@
digraph structs {
subgraph x {
Server [rank=min];
node [shape=record];
subgraph clusterqueue {
label = "Queue";
structqueue [shape=record,label="{ Position | 0 | 1 | 2 | 3 | 4 } | { Dataset | <f1> Car | Car | Person | Car | <f2> Car }|{ Datakey | Subaru | Ford | Simon | Ford | Subaru }|{ Operation | Set | Update | Set | Remove | Update }|{ Update | <f3> \{\"Seats\": \"Leather\"\} | \{...\} | \{...\} | \{...\} | <f4> \{ \"$inc \{ \"Likes\": 1 \}\} }"];
}
structqueue:f3 -> Server [color="red",style="bold"];
}
}
213 changes: 118 additions & 95 deletions README.md
Expand Up @@ -164,10 +164,9 @@ The set operation will add a Queueitem to the end of the Queue.

Because SyncIt does not know anything about the implementation details of the server pushing changes it a two stage process. These are:

1. Your App should request the next Queueitem that needs uploading to the Server from SyncIt then begin communicating that to the server ( SyncIt.getFirst() ).
2. The server will either Accept or Reject your Queueitem, assuming it is accepted your App should notify SyncIt it has been uploaded ( SyncIt.apply() ).
Your App should request the next Queueitem that needs uploading to the Server from SyncIt then begin communicating that to the server ( SyncIt.getFirst() ).

![Uploading to server and applying accepted changes](bin/README/img/getfirst_and_apply.png)
##### Code

```javascript
syncIt.getFirst(function(err,queueitem) {
Expand All @@ -184,10 +183,16 @@ syncIt.getFirst(function(err,queueitem) {
});
```

##### Diagram

![SyncIt.getFirst()](bin/README/img/getfirst.png)

Note: This will not change the Queue in any way.

Assuming the Queueitem is accepted by the Server, the first local Queueitem should then be applied to the local Store so that Dataset/Datakey matches the state on the Server in the Store and the Queueitem should then be removed.

##### Code

```javascript
jamesSyncIt.apply( function(err, appliedQueueitem, storedrecord ) {
if (err) {
Expand All @@ -210,6 +215,10 @@ jamesSyncIt.apply( function(err, appliedQueueitem, storedrecord ) {

![What happens during a syncIt.apply() - After](bin/README/img/apply-after.png)

##### Full overall diagram

![Uploading to server and applying accepted changes](bin/README/img/getfirst_and_apply.png)

### What happens if the data is modified by two different users / devices?

At some point, you will find that data has been modified by two different users or devices. The first thing to note about this is that, just like Subversion, Git or Mercurial SyncIt does not dictate how conflicts should be resolved but instead exposes a callback which exposes all required information for doing so. This is the middle parameter to the `SyncIt.feed()` function:
Expand Down Expand Up @@ -278,12 +287,14 @@ There is a (reasonably) complete set of [API Docs](http://forbesmyester.github.i

User James is sat on the the underground using an application developed using SyncIt. He is trying to decide what car to buy and the App performs the following change while out of mobile coverage.

jamesSyncIt.set(
'cars',
'Subaru',
{ color: 'blue' }
function(err) { if (err === SyncIt_Constant.Error.OK) { success(); } }
);
```javascript
jamesSyncIt.set(
'cars',
'Subaru',
{ color: 'blue' }
function(err) { if (err === SyncIt_Constant.Error.OK) { success(); } }
);
```

User | Dataset | Datakey | Store | Queueitem Update
------|---------|---------|-------|-----------------
Expand All @@ -297,28 +308,30 @@ James is happy because he is making progress on deciding on his next car.

Later, when James exits the underground the App detects that it can connect and makes the following API call:

syncIt.getFirst(function(err,queueitem) {
if (err !== SyncIt_Constant.Error.OK) {
// throw?
```javascript
syncIt.getFirst(function(err,queueitem) {
if (err !== SyncIt_Constant.Error.OK) {
// throw?
}
xhr(
'http://server/' + queueitem.s + '/' + queueitem.k,
{
method: 'PATCH',
...
}
xhr(
'http://server/' + queueitem.s + '/' + queueitem.k,
{
method: 'PATCH',
).then(
function() {
// data now stored on server
jamesSyncIt.apply(function(err) {
...
}
).then(
function() {
// data now stored on server
jamesSyncIt.apply(function(err) {
...
});
},
function(err) {
// something went wrong... throw err?
}
);
});
});
},
function(err) {
// something went wrong... throw err?
}
);
});
```

User | Dataset | Datakey | Store | Queueitem
------|---------|---------|----------------------------------------|-----------
Expand All @@ -332,11 +345,13 @@ The reason this is two steps as apposed to the one commit step for Subversion is

His wife, Emily is using the same App and either through a push notification or polling gets James's update from the server which calls:

emilySyncIt.feed(
[Queueitem], // The update from James
function( ... ) { ... }, // Conflict Resolution - We'll get to this soon
function(err) { ... }
);
```javascript
emilySyncIt.feed(
[Queueitem], // The update from James
function( ... ) { ... }, // Conflict Resolution - We'll get to this soon
function(err) { ... }
);
```

User | Dataset | Datakey | Store | Queueitem
------|---------|---------|----------------------------------------|-----------
Expand All @@ -347,11 +362,13 @@ This will add the change to the local store, assuming that there are no local ch

Emily does not like the idea of thier car being a Subaru and makes the following changes:

emilySyncIt.set(
'cars',
'Subaru',
{ color: 'blue', style: 'a bit too boy racer for Emily' }
);
```javascript
emilySyncIt.set(
'cars',
'Subaru',
{ color: 'blue', style: 'a bit too boy racer for Emily' }
);
```

User | Dataset | Datakey | Store | Queueitem
------|---------|---------|----------------------------------------|-----------
Expand All @@ -364,12 +381,14 @@ Because she is still in the park and has good mobile coverage that change is upl

James is again out of mobile coverage and is completely unaware of Emily's change but has discovered that Subaru's are four wheel drive...

jamesSyncIt.update(
'cars',
'Subaru',
{ $set: { pluspoints: ['has 4WD'] }, $inc { votes: 1 } },
function(err) { if (err === SyncIt_Constant.Error.OK) { success(); } }
);
```javascript
jamesSyncIt.update(
'cars',
'Subaru',
{ $set: { pluspoints: ['has 4WD'] }, $inc { votes: 1 } },
function(err) { if (err === SyncIt_Constant.Error.OK) { success(); } }
);
```

User | Dataset | Datakey | Store | Queueitem | Reads
------|---------|---------|----------------------------------------|---------------------------------------------------------------|------------
Expand All @@ -384,18 +403,20 @@ What is the correct course of action that James's App should take in this situat

Internally SyncIt stores everything including a Modifier and a Version. The data structure looks something like the following:

{
s: "cars", // dataset
k: "Subaru", // datakey
b: 1, // what version this Queueitem is based on (so this is version 2)
m: "james", // the user/device that made the change
o: "update", // the operation that was performed
t: 1369345483365, // timestamp when the operation was performed
u: { // the data for the operation
"$set": { "pluspoints": ["has 4WD"] },
"$inc" { "votes": 1 }
}
}
```javascript
{
s: "cars", // dataset
k: "Subaru", // datakey
b: 1, // what version this Queueitem is based on (so this is version 2)
m: "james", // the user/device that made the change
o: "update", // the operation that was performed
t: 1369345483365, // timestamp when the operation was performed
u: { // the data for the operation
"$set": { "pluspoints": ["has 4WD"] },
"$inc" { "votes": 1 }
}
}
```

So conflicts are possible to detect by comparing versions.

Expand All @@ -405,43 +426,45 @@ The second part of the solution is that `SyncIt.feed()` includes a callback para

The reason for conflict resolution being a callback function I feel it would be impossible for SyncIt to dictate and could be part of your core application logic. So the code/data for feeding data into SyncIt could end up looking something like the following:

SyncIt.feed(
[{
// The data which has been recieved from other parties via a server
"s": "cars",
"k": "Subaru",
"u": { color:'blue', style:'a bit too boy racer for Emily' },
"o": "set", "b": 1, "m": "emily", "t": 1369345483321
}],
function(dataset, datakey, stored, localChanges, remoteChanges, resolved) {
// This is a super basic, perhaps too basic, example of a conflict
// resolution function that will apply the local update on top of
// the remote update if it has a later timestamp
if (
localChanges[localChanges.length - 1].t >
remoteChanges[remoteChanges.length - 1].t
) {
// James made the last change, so blindly take it!
return resolved(
true,
[remoteChanges[remoteChanges.length - 1]]
);
}
// Emily made the last change, so we will throw away our changes
return resolved(true,[]);
},
function(err,remoteUpdatesNotFed) {
if (err === SyncIt_Constant.Error.OK) {
return success();
}
// err explains the reason for the error
// remoteUpdatesNotFed includes the update that are on the server
// which we could not feed due to the error.
```javascript
SyncIt.feed(
[{
// The data which has been recieved from other parties via a server
"s": "cars",
"k": "Subaru",
"u": { color:'blue', style:'a bit too boy racer for Emily' },
"o": "set", "b": 1, "m": "emily", "t": 1369345483321
}],
function(dataset, datakey, stored, localChanges, remoteChanges, resolved) {

// This is a super basic, perhaps too basic, example of a conflict
// resolution function that will apply the local update on top of
// the remote update if it has a later timestamp

if (
localChanges[localChanges.length - 1].t >
remoteChanges[remoteChanges.length - 1].t
) {
// James made the last change, so blindly take it!
return resolved(
true,
[localChanges[localChanges.length - 1]]
);
}
);

// Emily made the last change, so we will throw away our changes
return resolved(true,[]);
},
function(err,remoteUpdatesNotFed) {
if (err === SyncIt_Constant.Error.OK) {
return success();
}
// err explains the reason for the error
// remoteUpdatesNotFed includes the update that are on the server
// which we could not feed due to the error.
}
);
```

User | Dataset | Datakey | Store | Queueitem | Reads
------|---------|---------|----------------------------------------|---------------------------------------------------------------|------------
Expand All @@ -458,8 +481,8 @@ I need to do the following:
* Add a license to all files (It'll be MIT/BSD)
* Test demo (and everythign else!) in IE
* Client SyncIt
* localStorage for SyncIt (Store)
* Add Async wrappers for Store & Persist
* Add persistance to `allLocalToApplyAfterwards` in SyncIt.feed() (Important)
* Add Async wrappers for IndexedDb Store and Persist...
* Server SyncIt
* Create a real SyncItServer based on SyncItTestServer, it should be pretty easy, because SyncItTestServer is pretty abstracted.
* I want to make ServerPersist for both MongoDB and DynamoDB.
Expand Down
2 changes: 1 addition & 1 deletion README.work_locally_set.dot
Expand Up @@ -2,7 +2,7 @@ digraph a {

subgraph clusteryourcode {
label = "Your Code";
App [label="Request Data", rank=1];
App [label="Change Data", rank=1];
SyncItCallback [label=Callback];
SyncItEventHandler [label="Added to Queue listener"];
}
Expand Down
Binary file modified bin/README/img/apply-after.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bin/README/img/download_updates_from_server.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bin/README/img/feed_into_syncit.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added bin/README/img/getfirst.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bin/README/img/work_locally_set.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions build-docs
Expand Up @@ -13,6 +13,7 @@ cat README.download_updates_from_server.dot | dot -Gdpi=64 -Tpng:cairo:cairo > b
cat README.feed_into_syncit.dot | dot -Gdpi=64 -Tpng:cairo:cairo > bin/README/img/feed_into_syncit.png
cat README.work_locally_get.dot| dot -Gdpi=64 -Tpng:cairo:cairo > bin/README/img/work_locally_get.png
cat README.work_locally_set.dot| dot -Gdpi=64 -Tpng:cairo:cairo > bin/README/img/work_locally_set.png
cat README.getfirst.dot | dot -Gdpi=64 -Tpng:cairo:cairo > bin/README/img/getfirst.png
cat README.getfirst_and_apply.dot | dot -Gdpi=64 -Tpng:cairo:cairo > bin/README/img/getfirst_and_apply.png
dia -e bin/README/img/process.png -O /tmp README.process.dia
marked -i README.md -o README.html
Expand Down
6 changes: 3 additions & 3 deletions demo/public/index.html
Expand Up @@ -106,12 +106,12 @@ <h3>API</h3>
<nav class="tabs">
<dl>
<dd class="active tab download"><a href="#lhs-tab=download">
Download</a></dd>
<dd class="tab upload"><a href="#lhs-tab=upload">Upload</a></dd>
<dd class="tab apply"><a href="#lhs-tab=apply">Apply</a></dd>
Download & Feed</a></dd>
<dd class="tab set"><a href="#lhs-tab=set">Set</a></dd>
<dd class="tab update"><a href="#lhs-tab=update">Update</a></dd>
<dd class="tab remove"><a href="#lhs-tab=remove">Remove</a></dd>
<dd class="tab upload"><a href="#lhs-tab=upload">GetFirst &amp; Upload</a></dd>
<dd class="tab apply"><a href="#lhs-tab=apply">Apply</a></dd>
</dl>
</nav>
<ul class="tab-bodies">
Expand Down
3 changes: 1 addition & 2 deletions js/Queue/LocalStorage.js
@@ -1,3 +1,4 @@
/* jshint strict: true, smarttabs: true, es3: true, forin: true, immed: true, latedef: true, newcap: true, noarg: true, undef: true, unused: true, es3: true, bitwise: false, curly: true, latedef: true, newcap: true, noarg: true, noempty: true */
(function (root, factory) { // UMD from https://github.com/umdjs/umd/blob/master/returnExports.js
if (typeof exports === 'object') {
module.exports = factory(
Expand All @@ -15,7 +16,6 @@
}
})(this, function (SyncIt_Constant) {

/* jshint strict: true, smarttabs: true, es3: true, forin: true, immed: true, latedef: true, newcap: true, noarg: true, undef: true, unused: true, es3: true, bitwise: false, curly: true, latedef: true, newcap: true, noarg: true, noempty: true */
"use strict";

// Author: Matthew Forrester <matt_at_keyboardwritescode.com>
Expand All @@ -27,7 +27,6 @@
var Lsq = function(namespace,localStorage,stringifyFunc,parseFunc,maxOrderDigitLength) {
this._ns = namespace;
this._ls = localStorage;
this._listening = false;
this._stringifyFunc = stringifyFunc ?
stringifyFunc :
function(ob) {
Expand Down

0 comments on commit 65d879c

Please sign in to comment.