Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added options to disable live sync and support for periodic polling for changes #140

Open
wants to merge 37 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c842ce8
`Attachment` transform
Dec 14, 2015
f11ba27
Describe usage of `Attachment` transform
Dec 14, 2015
b6f074d
Test stubbed attachments
Dec 14, 2015
3e5f36f
Clarify the use of base-64 encoded String attachments
stevebest Dec 23, 2015
6090f13
Merge remote-tracking branch 'upstream/master' into stevebest
simonexmachina Aug 17, 2016
c397928
Update Bower dependencies (#137)
broerse Aug 30, 2016
38f25cf
Correct import of Ember Data model blueprint
backspace Jun 21, 2016
c5355e6
3.2.2
broerse Aug 30, 2016
3c33665
Describe usage of `Attachment` transform
stevebest Dec 14, 2015
95290bb
Keep the `length` and `digest` properties of a stub attachment
stevebest Aug 17, 2016
94f995e
Test that `digest` and `length` properties of a stub attachment are p…
stevebest Aug 17, 2016
f746fd8
Add missing property on a test object
stevebest Aug 17, 2016
3c9a072
Deserialize "no attachments" into an empty array
stevebest Aug 17, 2016
97ede1b
Is fix a typo
stevebest Aug 17, 2016
6df6ee6
Documentation for `defaultValue` of `attachment`
stevebest Aug 18, 2016
3c6162b
Return [] from serialize so that files can be added.
simonexmachina Aug 17, 2016
02ea2a0
Provide two transforms: `attachment` and `attachments`
simonexmachina Aug 17, 2016
b0fea2b
Update unit test
simonexmachina Aug 17, 2016
412311b
Change serializer so that attachments are passed to and from relation…
simonexmachina Aug 18, 2016
1de66b4
Ensure that length is available when the attachment is first saved
simonexmachina Aug 19, 2016
e57abbc
Added integration test
simonexmachina Aug 22, 2016
1445d0e
Tests passing in PhantomJS
simonexmachina Aug 22, 2016
c4e1550
Remove ember-data-1.13 from ember-try scenarios. Add Object.assign po…
simonexmachina Aug 23, 2016
44cd398
Check ember-data version and add install instructions for old ember-d…
simonexmachina Aug 30, 2016
400baaf
implement glue code for query and queryRecord
BernardTolosajr Jun 21, 2016
2707d01
changed selector to filter
BernardTolosajr Sep 4, 2016
ea6c8bf
Updated README and changelog
simonexmachina Sep 4, 2016
99ac186
Update README
broerse Sep 5, 2016
a55a4bc
4.0.0
broerse Sep 6, 2016
81b3f82
Fix Blueprint
broerse Sep 6, 2016
088b9f1
4.0.1
broerse Sep 6, 2016
2f0c8a3
Added option to disable live sync and support for periodic polling fo…
simonexmachina Sep 14, 2016
9dc00e7
Improve README
simonexmachina Sep 14, 2016
741bd75
Allow sync() to be used in conjunction with liveSync
simonexmachina Sep 14, 2016
60f4e92
Improve README
simonexmachina Sep 14, 2016
b986642
Fix Typo
simonexmachina Sep 23, 2016
960772b
Merge remote-tracking branch 'upstream/master' into sync-changes
simonexmachina Sep 26, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,27 @@ PouchDB.debug.enable('*');

See the [PouchDB sync API](http://pouchdb.com/api.html#sync) for full usage instructions.

### Live sync

PouchDB's live sync uses a long-polling socket, so you may find that you hit the browser's
limit of HTTP connections to a given host. In this case you may find that a periodic
sync is better for your needs.

If you are connecting directly to a database over HTTP without syncing to a local database
then ember-pouch's change detection will use a long-polling socket. The following will
poll for changes rather than using live sync.

```js
export default Adapter.extend({
db: db,
liveSync: false,
syncInterval: 5000
});
```

You can also sync manually by calling `adapter.sync()`, which returns a Promise that is
fulfilled on the `complete` event from PouchDB.

## EmberPouch Blueprints

### Model
Expand Down
92 changes: 76 additions & 16 deletions addon/adapters/pouch.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,106 @@ import {
} from 'ember-pouch/utils';

const {
assert,
run: {
bind
bind,
later,
cancel
},
on,
observer,
String: {
pluralize,
camelize,
classify
}
},
RSVP
} = Ember;

export default DS.RESTAdapter.extend({
coalesceFindRequests: true,
liveSync: true,
syncInterval: null,

liveChanges: null,
syncChanges: null,
syncTimer: null,
lastSeq: null,

// 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
// reloading redundant.
shouldReloadRecord: function () { return false; },
shouldBackgroundReloadRecord: function () { return false; },
_onInit : on('init', function() {
this._startChangesToStoreListener();
}),
_startChangesToStoreListener: function () {
_liveSyncHandler: observer('liveSync', 'db', function () {
if (this.liveChanges) {
this.liveChanges.cancel();
}
var db = this.get('db');
if (db) {
this.changes = db.changes({
if (this.get('liveSync') && db) {
this.liveChanges = db.changes({
since: 'now',
live: true,
returnDocs: false
}).on('change', bind(this, 'onChange'));
}
}),
_syncIntervalHandler: observer('syncInterval', 'db', function () {
cancel(this.syncTimer);
if (this.get('syncInterval')) {
assert("Only one of liveSync or syncInterval should be used for a given adapter",
!this.get('liveSync')
);
this._scheduleSync();
}
}),
_scheduleSync() {
cancel(this.syncTimer);
this.syncTimer = later(() => {
this.sync();
this._scheduleSync();
}, this.get('syncInterval'));
},
changeDb: function(db) {
if (this.changes) {
this.changes.cancel();
sync() {
var db = this.get('db');
if (!db) {
throw new Error("Can't sync without a db");
}

return (this.lastSeq ? RSVP.resolve(this.lastSeq) :
db.info().then(info => info.update_seq)
).then(sinceSeq => new RSVP.Promise((resolve, reject) => {
if (this.syncChanges) {
this.syncChanges.cancel();
}
this.syncChanges = db.changes({
since: sinceSeq,
returnDocs: false
}).on('complete', ev => {
this.lastSeq = ev.last_seq;
resolve(ev);
}).on('error', reject);
if (!this.get('liveSync')) {
this.syncChanges.on('change', bind(this, 'onChange'));
}
}));
},
_startSyncing: on('init', function() {
this._liveSyncHandler();
this._syncIntervalHandler();
}),
_stopSyncing() {
if (this.liveChanges) {
this.liveChanges.cancel();
}
if (this.syncTimer) {
cancel(this.syncTimer);
}
if (this.syncChanges) {
this.syncChanges.cancel();
}
},
changeDb: function(db) {
this._stopSyncing();
var store = this.store;
var schema = this._schema || [];

Expand All @@ -52,7 +115,6 @@ export default DS.RESTAdapter.extend({

this._schema = null;
this.set('db', db);
this._startChangesToStoreListener();
},
onChange: function (change) {
// If relational_pouch isn't initialized yet, there can't be any records
Expand Down Expand Up @@ -107,9 +169,7 @@ export default DS.RESTAdapter.extend({
},

willDestroy: function() {
if (this.changes) {
this.changes.cancel();
}
this._stopSyncing();
},

_init: function (store, type) {
Expand Down
14 changes: 14 additions & 0 deletions tests/dummy/app/adapters/hot-sauce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Adapter } from 'ember-pouch/index';
import PouchDB from 'pouchdb';

function createDb() {
return new PouchDB('hot-sauces');
}

export default Adapter.extend({
liveSync: false,
init() {
this._super(...arguments);
this.set('db', createDb());
}
});
6 changes: 6 additions & 0 deletions tests/dummy/app/models/hot-sauce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import DS from 'ember-data';

export default DS.Model.extend({
rev: DS.attr('string'),
name: DS.attr('string')
});
11 changes: 11 additions & 0 deletions tests/helpers/async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Ember from 'ember';

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

12 changes: 1 addition & 11 deletions tests/integration/adapters/pouch-default-change-watcher-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from 'qunit';
import moduleForIntegration from '../../helpers/module-for-acceptance';

import { promiseToRunLater } from '../../helpers/async';
import Ember from 'ember';

/*
Expand All @@ -23,15 +23,6 @@ moduleForIntegration('Integration | Adapter | Default Change Watcher', {
}
});

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

test('a loaded instance automatically reflects directly-made database changes', function (assert) {
assert.expect(2);
var done = assert.async();
Expand Down Expand Up @@ -193,4 +184,3 @@ test('a new record is automatically loaded', function (assert) {
}, 15);
}).finally(done);
});

118 changes: 118 additions & 0 deletions tests/integration/adapters/without-live-sync-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import Ember from 'ember';
import { test } from 'qunit';
import moduleForIntegration from '../../helpers/module-for-acceptance';
import { promiseToRunLater } from '../../helpers/async';

moduleForIntegration('Integration | Adapter | Without live sync', {
beforeEach(assert) {
var done = assert.async();

this.adapter = function adapter() {
return this.store().adapterFor('hot-sauce');
};
this.db = function db() {
return this.adapter().get('db');
};
Ember.RSVP.Promise.resolve().then(() => {
return this.db().bulkDocs([
{ _id: 'hotSauce_2_A', data: { name: 'Cholula' } },
{ _id: 'hotSauce_2_B', data: { name: 'Melbourne Hot Sauce' } },
]);
}).finally(done);
},
afterEach(assert) {
var done = assert.async();
this.db().destroy().then(() => {
Ember.run(() => this.adapter().destroy());
done();
});
}
});

test('changes are not synced', function (assert) {
assert.expect(2);
var done = assert.async();

Ember.RSVP.resolve().then(() => {
return this.store().find('hot-sauce', 'A');
}).then((hotSauce) => {
assert.equal('Cholula', hotSauce.get('name'),
'the loaded instance should reflect the initial test data');

return this.db().get('hotSauce_2_A');
}).then((hotSauceRecord) => {
hotSauceRecord.data.name = 'Death Sauce';
return this.db().put(hotSauceRecord);
}).then(() => {
return promiseToRunLater(() => {
var alreadyLoadedHotSauce = this.store().peekRecord('hot-sauce', 'A');
assert.equal(alreadyLoadedHotSauce.get('name'), 'Cholula',
'the loaded instance should not automatically reflect the change in the database');
}, 15);
}).finally(done);
});

test('changes can be manually synced', function (assert) {
assert.expect(3);
var done = assert.async();

this.adapter().set('liveSync', false);
Ember.RSVP.resolve().then(() => {
return this.adapter().sync(); // perform initial sync to get update_seq
}).then(() => {
return this.store().find('hot-sauce', 'A');
}).then((hotSauce) => {
assert.equal('Cholula', hotSauce.get('name'),
'the loaded instance should reflect the initial test data');
return this.db().get('hotSauce_2_A');
}).then((hotSauceRecord) => {
hotSauceRecord.data.name = 'Death Sauce';
return this.db().put(hotSauceRecord);
}).then(() => {
return this.store().find('hot-sauce', 'A');
}).then((hotSauce) => {
assert.equal('Cholula', hotSauce.get('name'),
'the loaded instance does not reflect the changes');
return this.adapter().sync();
})
.then(() => {
return promiseToRunLater(() => {
var alreadyLoadedHotSauce = this.store().peekRecord('hot-sauce', 'A');
assert.equal(alreadyLoadedHotSauce.get('name'), 'Death Sauce',
'the loaded instance reflects the change in the database');
}, 500);
}).finally(done);
});

test('changes can be synced periodically', function (assert) {
assert.expect(3);
var done = assert.async();

this.adapter().set('liveSync', false);
Ember.RSVP.resolve().then(() => {
return this.adapter().sync(); // perform initial sync to get update_seq
}).then(() => {
return this.store().find('hot-sauce', 'A');
}).then((hotSauce) => {
assert.equal('Cholula', hotSauce.get('name'),
'the loaded instance should reflect the initial test data');
return this.db().get('hotSauce_2_A');
}).then((hotSauceRecord) => {
hotSauceRecord.data.name = 'Death Sauce';
return this.db().put(hotSauceRecord);
}).then(() => {
return this.store().find('hot-sauce', 'A');
}).then((hotSauce) => {
assert.equal('Cholula', hotSauce.get('name'),
'the loaded instance does not reflect the changes');
this.adapter().set('syncInterval', 100);
})
.then(() => {
return promiseToRunLater(() => {
var alreadyLoadedHotSauce = this.store().peekRecord('hot-sauce', 'A');
assert.equal(alreadyLoadedHotSauce.get('name'), 'Death Sauce',
'the loaded instance reflects the change in the database');
}, 500);
}).finally(done);
});