Skip to content

Commit

Permalink
Merge pull request #192 from jlami/eventually-consistent
Browse files Browse the repository at this point in the history
Eventually consistent
  • Loading branch information
broerse authored Sep 8, 2017
2 parents ceabf61 + b8025dd commit 0506e43
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 45 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ env:
- EMBER_TRY_SCENARIO=ember-lts-2.8
- EMBER_TRY_SCENARIO=ember-2.10-stack
- EMBER_TRY_SCENARIO=ember-lts-2.12
- EMBER_TRY_SCENARIO=ember-2.14-stack
- EMBER_TRY_SCENARIO=ember-release
- EMBER_TRY_SCENARIO=ember-beta
- EMBER_TRY_SCENARIO=ember-canary
# - EMBER_TRY_SCENARIO=ember-canary

matrix:
fast_finish: true
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@ This project was originally based on the [ember-data-hal-adapter](https://github
And of course thanks to all our wonderful contributors, [here](https://github.com/pouchdb-community/ember-pouch/graphs/contributors) and [in Relational Pouch](https://github.com/pouchdb-community/relational-pouch/graphs/contributors)!

## Changelog
* **5.0.0-beta.1**
- Eventually consistency added: documents that are not in the database will result in an 'eternal' promise. This promise will only resolve when an entry for that document is found. Deleted documents will also satisfy this promise. This mirrors the way that couchdb replication works, because the changes might not come in the order that ember-data expects. Foreign keys might therefor point to documents that have not been loaded yet. Ember-data normally resets these to null, but keeping the promise in a loading state will keep the relations intact until the actual data is loaded.
* **4.3.0**
- Bundle pouchdb-find [#191](https://github.com/pouchdb-community/ember-pouch/pull/191)
* **4.2.9**
Expand Down
61 changes: 57 additions & 4 deletions addon/adapters/pouch.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Ember from 'ember';
import DS from 'ember-data';
//import BelongsToRelationship from 'ember-data/-private/system/relationships/state/belongs-to';

import {
extractDeleteRecord
Expand All @@ -18,8 +19,17 @@ const {
}
} = Ember;

//BelongsToRelationship.reopen({
// findRecord() {
// return this._super().catch(() => {
// //not found: deleted
// this.clear();
// });
// }
//});

export default DS.RESTAdapter.extend({
coalesceFindRequests: true,
coalesceFindRequests: false,

// The change listener ensures that individual records are kept up to date
// when the data in the database changes. This makes ember-data 2.0's record
Expand Down Expand Up @@ -66,6 +76,17 @@ export default DS.RESTAdapter.extend({

var store = this.store;

if (this.waitingForConsistency[change.id]) {
let promise = this.waitingForConsistency[change.id];
delete this.waitingForConsistency[change.id];
if (change.deleted) {
promise.reject("deleted");
} else {
promise.resolve(this._findRecord(obj.type, obj.id));
}
return;
}

try {
store.modelFor(obj.type);
} catch (e) {
Expand Down Expand Up @@ -361,7 +382,11 @@ export default DS.RESTAdapter.extend({
findRecord: function (store, type, id) {
this._init(store, type);
var recordTypeName = this.getRecordTypeName(type);
return this.get('db').rel.find(recordTypeName, id).then(function (payload) {
return this._findRecord(recordTypeName, id);
},

_findRecord(recordTypeName, id) {
return this.get('db').rel.find(recordTypeName, id).then(payload => {
// Ember Data chokes on empty payload, this function throws
// an error when the requested data is not found
if (typeof payload === 'object' && payload !== null) {
Expand All @@ -373,8 +398,36 @@ export default DS.RESTAdapter.extend({
return payload;
}
}
throw new Error('Not found: type "' + recordTypeName +
'" with id "' + id + '"');

return this._eventuallyConsistent(recordTypeName, id);
});
},

//TODO: cleanup promises on destroy or db change?
waitingForConsistency: {},
_eventuallyConsistent: function(type, id) {
let pouchID = this.get('db').rel.makeDocID({type, id});
let defer = Ember.RSVP.defer();
this.waitingForConsistency[pouchID] = defer;

return this.get('db').rel.isDeleted(type, id).then(deleted => {
//TODO: should we test the status of the promise here? Could it be handled in onChange already?
if (deleted) {
delete this.waitingForConsistency[pouchID];
throw "Document of type '" + type + "' with id '" + id + "' is deleted.";
} else if (deleted === null) {
return defer.promise;
} else {
Ember.assert('Status should be existing', deleted === false);
//TODO: should we reject or resolve the promise? or does JS GC still clean it?
if (this.waitingForConsistency[pouchID]) {
delete this.waitingForConsistency[pouchID];
return this._findRecord(type, id);
} else {
//findRecord is already handled by onChange
return defer.promise;
}
}
});
},

Expand Down
35 changes: 22 additions & 13 deletions config/ember-try.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,31 +64,40 @@ module.exports = {
},
},
{
name: 'ember-release',
name: 'ember-2.14-stack',
npm: {
devDependencies: {
'ember-data': 'components/ember-data#release',
'ember-source': 'components/ember#release',
},
'ember-data': '2.14.10',
'ember-source': '2.14.1',
}
},
},
{
name: 'ember-beta',
name: 'ember-release',
npm: {
devDependencies: {
'ember-data': 'components/ember-data#beta',
'ember-source': 'components/ember#beta',
'ember-data': 'latest',
'ember-source': 'latest',
},
},
}
},
{
name: 'ember-canary',
name: 'ember-beta',
npm: {
devDependencies: {
'ember-data': 'components/ember-data#canary',
'ember-source': 'components/ember#canary',
'ember-data': 'beta',
'ember-source': 'beta',
},
},
}
}
},
// {
// name: 'ember-canary',
// npm: {
// devDependencies: {
// 'ember-data': 'components/ember-data#canary',
// 'ember-source': 'components/ember#canary',
// },
// },
// }
]
};
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ember-pouch",
"version": "4.3.0",
"version": "5.0.0-beta.1",
"description": "PouchDB adapter for Ember Data",
"directories": {
"doc": "doc",
Expand Down Expand Up @@ -47,10 +47,10 @@
"ember-cli-sri": "^2.1.0",
"ember-cli-test-loader": "^1.1.0",
"ember-cli-uglify": "^1.2.0",
"ember-data": "2.14.4",
"ember-data": "2.14.10",
"ember-disable-prototype-extensions": "^1.1.2",
"ember-export-application-global": "^2.0.0",
"ember-inflector": "^1.9.4",
"ember-inflector": "^2.0.0",
"ember-load-initializers": "^1.0.0",
"ember-resolver": "^4.0.0",
"ember-source": "~2.14.1",
Expand All @@ -59,7 +59,7 @@
"dependencies": {
"broccoli-stew": "^1.3.1",
"pouchdb": "^6.3.4",
"relational-pouch": "^2.0.0",
"relational-pouch": "jlami/relational-pouch#isDeleted",
"ember-cli-babel": "^6.3.0"
},
"engines": {
Expand Down
1 change: 1 addition & 0 deletions tests/dummy/config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = function(environment) {
FEATURES: {
// Here you can enable experimental features on an ember canary build
// e.g. 'with-controller': true
'ds-references': true,
},
EXTEND_PROTOTYPES: {
// Prevent Ember Data from overriding Date.parse.
Expand Down
148 changes: 125 additions & 23 deletions tests/integration/adapters/pouch-basics-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,43 @@ import Ember from 'ember';

import config from 'dummy/config/environment';

function promiseToRunLater(timeout) {
return new Ember.RSVP.Promise((resolve) => {
Ember.run.later(() => {
resolve();
}, timeout);
});
}

//function delayPromise(timeout) {
// return function(res) {
// return promiseToRunLater(timeout).then(() => res);
// }
//}


function savingHasMany() {
return !config.emberpouch.dontsavehasmany;
}

function getDocsForRelations() {
let result = [];

let c = { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } };
if (savingHasMany()) { c.data.ingredients = ['X', 'Y']; }
result.push(c);

let d = { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } };
if (savingHasMany()) { d.data.ingredients = ['Z']; }
result.push(d);

result.push({ _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'C' }});
result.push({ _id: 'foodItem_2_Y', data: { name: 'pork loin', soup: 'C' }});
result.push({ _id: 'foodItem_2_Z', data: { name: 'black beans', soup: 'D' }});

return result;
}

/*
* Tests basic CRUD behavior for an app using the ember-pouch adapter.
*/
Expand Down Expand Up @@ -143,28 +180,6 @@ test('queryRecord returns null when no record is found', function (assert) {
});
});

function savingHasMany() {
return !config.emberpouch.dontsavehasmany;
}

function getDocsForRelations() {
let result = [];

let c = { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } };
if (savingHasMany()) { c.data.ingredients = ['X', 'Y']; }
result.push(c);

let d = { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } };
if (savingHasMany()) { d.data.ingredients = ['Z']; }
result.push(d);

result.push({ _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'C' }});
result.push({ _id: 'foodItem_2_Y', data: { name: 'pork loin', soup: 'C' }});
result.push({ _id: 'foodItem_2_Z', data: { name: 'black beans', soup: 'D' }});

return result;
}

test('can query one record', function (assert) {
assert.expect(1);

Expand Down Expand Up @@ -335,14 +350,101 @@ test('delete an existing record', function (assert) {
}).finally(done);
});

};

let asyncTests = function() {

test('eventually consistency - success', function (assert) {
assert.timeout(5000);
var done = assert.async();
Ember.RSVP.Promise.resolve().then(() => {
return this.db().bulkDocs([
{ _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'C' }},
//{_id: 'tacoSoup_2_C', data: { flavor: 'test' } }
]);
})
.then(() => this.store().findRecord('food-item', 'X'))
.then(foodItem => {
let result = [
foodItem.get('soup')
.then(soup => assert.equal(soup.id, 'C')),

promiseToRunLater(0)
.then(() => {
return this.db().bulkDocs([
{_id: 'tacoSoup_2_C', data: { flavor: 'test' } }
]);}),
];

return Ember.RSVP.all(result);
})
.finally(done);
});

test('eventually consistency - deleted', function (assert) {
assert.timeout(5000);
var done = assert.async();
Ember.RSVP.Promise.resolve().then(() => {
return this.db().bulkDocs([
{ _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'C' }},
//{_id: 'tacoSoup_2_C', data: { flavor: 'test' } }
]);
})
.then(() => this.store().findRecord('food-item', 'X'))
.then(foodItem => {
let result = [
foodItem.get('soup')
.then((soup) => assert.ok(soup === null, 'isDeleted'))
.catch(() => assert.ok(true, 'isDeleted')),

promiseToRunLater(100)
.then(() => this.db().bulkDocs([
{_id: 'tacoSoup_2_C', _deleted: true }
])),
];

return Ember.RSVP.all(result);
})
.finally(done);
});

//TODO: only do this for async or dontsavehasmany?
test('delete cascade null', function (assert) {
assert.timeout(5000);
assert.expect(2);

var done = assert.async();
Ember.RSVP.Promise.resolve().then(() => {
return this.db().bulkDocs(getDocsForRelations());
})
// .then(() => this.store().findRecord('food-item', 'Z'))//prime ember-data store with Z
// .then(found => found.get('soup'))//prime belongsTo
.then(() => this.store().findRecord('taco-soup', 'D'))
.then((found) => {
return found.destroyRecord();
}).then(() => {
this.store().unloadAll();//to make sure the record is unloaded, normally this would be done by onChange listeren
return this.store().findRecord('food-item', 'Z');//Z should be updated now
})
.then((found) => {
return Ember.RSVP.Promise.resolve(found.get('soup')).catch(() => null).then((soup) => {
assert.ok(!found.belongsTo || found.belongsTo('soup').id() === null,
'should set id of belongsTo to null');
return soup;
});
}).then((soup) => {
assert.ok(soup === null,
'deleted soup should have cascaded to a null value for the belongsTo');
}).finally(done);
});
};

let syncAsync = function() {
module('async', {
beforeEach: function() {
config.emberpouch.async = true;
}
}, allTests);
}, () => { allTests(); asyncTests(); });
module('sync', {
beforeEach: function() {
config.emberpouch.async = false;
Expand Down

0 comments on commit 0506e43

Please sign in to comment.