├── .bowerrc ├── .editorconfig ├── .ember-cli ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── .watchmanconfig ├── LICENSE.md ├── README.md ├── addon ├── .gitkeep ├── adapters │ └── base.js ├── config.js ├── jobs │ ├── ajax.js │ ├── localforage.js │ └── rest.js ├── logics │ └── sync-loads.js ├── mixins │ ├── base.js │ ├── job.js │ ├── localstorage-id.js │ ├── offline.js │ └── online.js ├── queue.js ├── request.js └── utils │ ├── debug.js │ ├── erase-offline.js │ ├── expired.js │ ├── extract-online.js │ ├── handle-api-errors.js │ ├── is-object-empty.js │ ├── meta.js │ └── persist-offline.js ├── app ├── .gitkeep ├── blueprints │ └── ember-data-offline.js ├── initializers │ └── edo-request.js └── services │ └── store.js ├── blueprints ├── .jshintrc └── ember-data-offline │ └── index.js ├── bower.json ├── config ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── package.json ├── testem.json ├── tests ├── .jshintrc ├── acceptance │ ├── crud-test.js │ └── stress-test.js ├── dummy │ ├── app │ │ ├── adapters │ │ │ ├── application.js │ │ │ ├── car.js │ │ │ ├── company.js │ │ │ ├── office.js │ │ │ └── user.js │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── mirage │ │ │ ├── config.js │ │ │ ├── factories │ │ │ │ ├── car.js │ │ │ │ ├── city.js │ │ │ │ ├── company.js │ │ │ │ ├── office.js │ │ │ │ └── user.js │ │ │ └── scenarios │ │ │ │ └── default.js │ │ ├── models │ │ │ ├── .gitkeep │ │ │ ├── car.js │ │ │ ├── city.js │ │ │ ├── company.js │ │ │ ├── office.js │ │ │ └── user.js │ │ ├── pods │ │ │ ├── cities │ │ │ │ ├── route.js │ │ │ │ └── template.hbs │ │ │ ├── companies │ │ │ │ ├── route.js │ │ │ │ └── template.hbs │ │ │ ├── offices │ │ │ │ ├── route.js │ │ │ │ └── template.hbs │ │ │ ├── stress │ │ │ │ ├── route.js │ │ │ │ └── template.hbs │ │ │ └── users │ │ │ │ ├── index │ │ │ │ ├── controller.js │ │ │ │ ├── route.js │ │ │ │ └── template.hbs │ │ │ │ ├── template.hbs │ │ │ │ └── user │ │ │ │ ├── route.js │ │ │ │ └── template.hbs │ │ ├── router.js │ │ ├── routes │ │ │ ├── .gitkeep │ │ │ └── application.js │ │ ├── serializers │ │ │ ├── application.js │ │ │ ├── city.js │ │ │ └── user.js │ │ ├── styles │ │ │ └── app.css │ │ ├── templates │ │ │ ├── application.hbs │ │ │ └── components │ │ │ │ └── .gitkeep │ │ └── transforms │ │ │ └── json.js │ ├── config │ │ └── environment.js │ └── public │ │ ├── crossdomain.xml │ │ └── robots.txt ├── helpers │ ├── assert-meta.js │ ├── base.js │ ├── job.js │ ├── lf-utils.js │ ├── localforage-helpers.js │ ├── offline.js │ ├── resolver.js │ └── start-app.js ├── index.html ├── test-helper.js └── unit │ ├── .gitkeep │ ├── jobs │ ├── localstorage-test.js │ └── rest-test.js │ ├── mixins │ ├── base-test.js │ ├── job-test.js │ ├── offline-test.js │ └── online-test.js │ └── queue-test.js ├── vendor └── .gitkeep └── yuidoc.json /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.js] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.hbs] 21 | insert_final_newline = false 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [*.css] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [*.html] 30 | indent_style = space 31 | indent_size = 2 32 | 33 | [*.{diff,md}] 34 | trim_trailing_whitespace = false 35 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log 17 | testem.log 18 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "server", 4 | "document", 5 | "window", 6 | "-Promise" 7 | ], 8 | "browser": true, 9 | "boss": true, 10 | "curly": true, 11 | "debug": false, 12 | "devel": true, 13 | "eqeqeq": true, 14 | "evil": true, 15 | "forin": false, 16 | "immed": false, 17 | "laxbreak": false, 18 | "newcap": true, 19 | "noarg": true, 20 | "noempty": false, 21 | "nonew": false, 22 | "nomen": false, 23 | "onevar": false, 24 | "plusplus": false, 25 | "regexp": false, 26 | "undef": true, 27 | "sub": true, 28 | "strict": false, 29 | "white": false, 30 | "eqnull": true, 31 | "esnext": true, 32 | "unused": true 33 | } 34 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | tests/ 3 | tmp/ 4 | dist/ 5 | 6 | .bowerrc 7 | .editorconfig 8 | .ember-cli 9 | .travis.yml 10 | .npmignore 11 | **/.gitkeep 12 | bower.json 13 | Brocfile.js 14 | testem.json 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "0.12" 5 | 6 | sudo: false 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | env: 13 | - EMBER_TRY_SCENARIO=default 14 | 15 | matrix: 16 | fast_finish: true 17 | 18 | before_install: 19 | - export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH 20 | - "npm config set spin false" 21 | - "npm install -g npm@^2" 22 | 23 | install: 24 | - npm install -g bower 25 | - npm install 26 | - bower install 27 | 28 | script: 29 | - ember try $EMBER_TRY_SCENARIO test 30 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember-data-offline 2 | [![Build Status](https://travis-ci.org/api-hogs/ember-data-offline.svg?branch=master)](https://travis-ci.org/api-hogs/ember-data-offline) 3 | [![npm version](https://badge.fury.io/js/ember-data-offline.svg)](http://badge.fury.io/js/ember-data-offline) 4 | [![Ember Observer Score](http://emberobserver.com/badges/ember-data-offline.svg)](http://emberobserver.com/addons/ember-data-offline) 5 | [![Stories in Ready](https://badge.waffle.io/api-hogs/ember-data-offline.svg?label=ready&title=Ready)](http://waffle.io/api-hogs/ember-data-offline) 6 | 7 | Ember-data-offline is an addon that extends ember-data to work in offline mode. 8 | 9 | It caches records in the local storage (IndexedDB or equivalents). 10 | 11 | ## Installation 12 | 13 | ``` 14 | ember install ember-data-offline 15 | ``` 16 | 17 | ## Setup 18 | 19 | First, define your application adapter with offline support: 20 | 21 | ```javascript 22 | //app/adapters/application.js 23 | 24 | import baseAdapter from 'ember-data-offline/adapters/base'; 25 | 26 | export default baseAdapter.extend({ 27 | offlineNamespace: 'foo'//optional 28 | }); 29 | ``` 30 | 31 | Then define a model and a serializer for it: 32 | 33 | ```javascript 34 | //app/serializers/application.js 35 | 36 | import DS from 'ember-data'; 37 | 38 | export default DS.RESTSerializer.extend({ 39 | }); 40 | ``` 41 | 42 | If your primary key is different from `'id'`, you have to specify it in the adapter and serializer: 43 | 44 | ```javascript 45 | // in adapter: 46 | 47 | export default appAdapter.extend({ 48 | serializerPrimaryKey: '_id', 49 | }); 50 | ``` 51 | 52 | For more information, please, take look at dummy app. 53 | 54 | ## Details 55 | 56 | All syncornizations between local storage and backend are queued and performed sequentially. 57 | 58 | ## Contribution 59 | 60 | 1. fork repo 61 | 2. `git clone git@github.com:your-github/ember-data-offline.git` 62 | 2. `npm i && bower install` 63 | 3. add your feature 64 | 4. cover with tests 65 | 5. send PR! 66 | 67 | ## License 68 | 69 | [Licensed under MIT license] [1] 70 | 71 | [1]:http://opensource.org/licenses/mit-license.php 72 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/api-hogs/ember-data-offline/920597742e88d6961ce7cf34332d308a458dc533/addon/.gitkeep -------------------------------------------------------------------------------- /addon/adapters/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module ember-data-offline 3 | **/ 4 | import DS from 'ember-data'; 5 | import Ember from 'ember'; 6 | import offlineMixin from 'ember-data-offline/mixins/offline'; 7 | import onlineMixin from 'ember-data-offline/mixins/online'; 8 | import LFAdapter from 'ember-localforage-adapter/adapters/localforage'; 9 | import LFSerializer from 'ember-localforage-adapter/serializers/localforage'; 10 | import isObjectEmpty from 'ember-data-offline/utils/is-object-empty'; 11 | /** 12 | A base adapter, that can be used as-is or extended if necessary. 13 | 14 | @class BaseAdapter 15 | @extends DS.RESTAdapter 16 | @uses onlineMixin 17 | @constructor 18 | **/ 19 | export default DS.RESTAdapter.extend(onlineMixin, { 20 | __adapterName__: "ONLINE", 21 | offlineAdapter: null, 22 | 23 | initRunner: Ember.on('init', function() { 24 | let adapter = this; 25 | let container = this.container; 26 | 27 | let serializer = LFSerializer.extend({ 28 | normalize(typeClass) { 29 | let store = container.lookup('service:store'); 30 | let modelSerializer = store.serializerFor(typeClass.modelName); 31 | return modelSerializer.normalize.apply(modelSerializer, arguments); 32 | }, 33 | serialize(snapshot, options) { 34 | let json = this._super.apply(this, arguments); 35 | let store = snapshot.record.store; 36 | let modelSerializer = store.serializerFor(snapshot._internalModel.modelName); 37 | let primaryKey = 'id'; 38 | if (modelSerializer) { 39 | primaryKey = modelSerializer.primaryKey; 40 | let serialized = modelSerializer.serialize(snapshot, options); 41 | json = Ember.merge(json, serialized); 42 | } 43 | if (snapshot.get('__data_offline_meta__')) { 44 | json['__data_offline_meta__'] = snapshot.get('__data_offline_meta__'); 45 | } 46 | if (primaryKey !== 'id') { 47 | json.id = json[primaryKey]; 48 | } 49 | snapshot.eachRelationship((name, relationship) => { 50 | if (relationship.kind === 'hasMany' && Ember.isEmpty(json[name])) { 51 | json[name] = []; 52 | } 53 | }); 54 | return json; 55 | }, 56 | extractMeta: function(store, modelClass, payload) { 57 | let meta = store.metadataFor(modelClass); 58 | if (isObjectEmpty(meta)) { 59 | meta = {}; 60 | meta['__data_offline_meta__'] = {}; 61 | } 62 | if (Ember.isArray(payload)) { 63 | payload.forEach(_payload => { 64 | meta['__data_offline_meta__'][_payload[store.serializerFor(modelClass).primaryKey]] = _payload['__data_offline_meta__']; 65 | }); 66 | } else { 67 | meta['__data_offline_meta__'][payload[store.serializerFor(modelClass).primaryKey]] = payload['__data_offline_meta__']; 68 | } 69 | store.setMetadataFor(modelClass, meta); 70 | }, 71 | }).create({ 72 | container: container, 73 | }); 74 | let serializerPrimaryKey = this.get('serializerPrimaryKey'); 75 | if (serializerPrimaryKey) { 76 | serializer.set('primaryKey', serializerPrimaryKey); 77 | } 78 | let defaults = { 79 | __adapterName__: "OFFLINE", 80 | onlineAdapter: adapter, 81 | container: container, 82 | serializer: serializer, 83 | caching: 'none', 84 | namespace: 'ember-data-offline:store', 85 | }; 86 | if (adapter.offlineNamespace) { 87 | defaults.namespace = adapter.offlineNamespace; 88 | } 89 | let offlineAdapter = LFAdapter.extend(offlineMixin).create(defaults); 90 | 91 | this.set('offlineAdapter', offlineAdapter); 92 | }), 93 | }); 94 | -------------------------------------------------------------------------------- /addon/config.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | var defaults = { 4 | enabled: true, 5 | }; 6 | 7 | export default Ember.Object.extend({ 8 | withCustom: Ember.computed('custom', function() { 9 | return Ember.merge(defaults, this.get('custom')); 10 | }), 11 | isEnabled: Ember.computed('withCustom', function() { 12 | return this.get('withCustom.enabled'); 13 | }) 14 | }); 15 | -------------------------------------------------------------------------------- /addon/jobs/ajax.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import jobMixin from 'ember-data-offline/mixins/job'; 3 | 4 | export default Ember.Object.extend(jobMixin, { 5 | task() { 6 | return Ember.$.ajax(this.get('params')); 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /addon/jobs/localforage.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | @module jobs 4 | **/ 5 | import Ember from 'ember'; 6 | import jobMixin from 'ember-data-offline/mixins/job'; 7 | import persistOffline from 'ember-data-offline/utils/persist-offline'; 8 | /** 9 | An implementation of a syncronization job for the Localforage storage. 10 | 11 | @class Localforage 12 | @extends Ember.Object 13 | @uses Job 14 | @constructor 15 | **/ 16 | export default Ember.Object.extend(jobMixin, { 17 | /** 18 | A method called by default to execute the job. 19 | The method to be called as well as it's arguments can be customized. 20 | @method task 21 | @return promise {Promise} 22 | **/ 23 | task() { 24 | if (this[this.get('method')]){ 25 | return this[this.get('method')].apply(this, this.get('params')); 26 | } 27 | return this.get('adapter')[this.get('method')].apply(this.get('adapter'), this.get('params')); 28 | }, 29 | 30 | /** 31 | Saves the loaded records to the local storage. 32 | @method findAll 33 | @param store {DS.Store} 34 | @param typeClass {DS.Model} 35 | **/ 36 | findAll(store, typeClass) { 37 | persistOffline(this.get('adapter'), store, typeClass, null, 'findAll'); 38 | }, 39 | /** 40 | Saves the loaded record to the local storage. 41 | @method find 42 | @param store {DS.Store} 43 | @param typeClass {DS.Model} 44 | @param id {String|Number} 45 | **/ 46 | find(store, typeClass, id) { 47 | let adapter = this.get('adapter'); 48 | persistOffline(adapter, store, typeClass, id, "find"); 49 | }, 50 | /** 51 | Saves the loaded record to the local storage. 52 | @method findQuery 53 | @param store {DS.Store} 54 | @param typeClass {DS.Model} 55 | @param query {Object} 56 | @param onlineResp {Promise} 57 | **/ 58 | findQuery(store, typeClass, query, onlineResp) { 59 | let adapter = this.get('adapter'); 60 | persistOffline(adapter, store, typeClass, onlineResp, "findQuery"); 61 | }, 62 | 63 | /** 64 | Saves the loaded records to the local storage. 65 | @method findMany 66 | @param store {DS.Store} 67 | @param typeClass {DS.Model} 68 | @param ids {Array} 69 | **/ 70 | findMany(store, typeClass, ids) { 71 | let adapter = this.get('adapter'); 72 | persistOffline(adapter, store, typeClass, ids, 'findMany'); 73 | }, 74 | /** 75 | Saves the loaded record to the local storage. 76 | 77 | @method createRecord 78 | @param store {DS.Store} 79 | @param type {DS.Model} 80 | @param snapshot {DS.Snapshot} 81 | @param onlineResp {Promise} 82 | **/ 83 | createRecord(store, type, snapshot, onlineResp){ 84 | onlineResp.then(() => { 85 | return this.get('adapter').createRecord(store, type, snapshot); 86 | }); 87 | }, 88 | /** 89 | Updates the record in the local storage. 90 | 91 | @method updateRecord 92 | @param store {DS.Store} 93 | @param type {DS.Model} 94 | @param snapshot {DS.Snapshot} 95 | @param onlineResp {Promise} 96 | **/ 97 | updateRecord(store, type, snapshot, onlineResp){ 98 | onlineResp.then(() => { 99 | return this.get('adapter').updateRecord(store, type, snapshot); 100 | }); 101 | }, 102 | /** 103 | Deletes the record from the local storage. 104 | 105 | @method deleteRecord 106 | @param store {DS.Store} 107 | @param type {DS.Model} 108 | @param snapshot {DS.Snapshot} 109 | @param onlineResp {Promise} 110 | **/ 111 | deleteRecord(store, type, snapshot, onlineResp){ 112 | onlineResp.then(() => { 113 | return this.get('adapter').deleteRecord(store, type, snapshot); 114 | }); 115 | }, 116 | }); 117 | -------------------------------------------------------------------------------- /addon/jobs/rest.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module jobs 3 | **/ 4 | import Ember from 'ember'; 5 | import jobMixin from 'ember-data-offline/mixins/job'; 6 | import handleApiErrors from 'ember-data-offline/utils/handle-api-errors'; 7 | import { persistOne } from 'ember-data-offline/utils/persist-offline'; 8 | import { eraseOne } from 'ember-data-offline/utils/erase-offline'; 9 | import extractTargetRecordFromPayload from 'ember-data-offline/utils/extract-online'; 10 | /** 11 | A job that syncronizes the local storage with the backend. 12 | @class Rest 13 | @extends Ember.Object 14 | @uses Job 15 | @constructor 16 | **/ 17 | export default Ember.Object.extend(jobMixin, { 18 | /** 19 | A method called by default to execute the job. 20 | The method to be called as well as it's arguments can be customized. 21 | @method task 22 | @return promise {Promise} 23 | **/ 24 | task() { 25 | if (this[this.get('method')]){ 26 | return this[this.get('method')].apply(this, this.get('params')); 27 | } 28 | return this.get('adapter')[this.get('method')].apply(this.get('adapter'), this.get('params')); 29 | }, 30 | /** 31 | Saves the data received from the backend to the local storage. 32 | @method findAll 33 | @param store {DS.Store} 34 | @param typeClass {DS.Model} 35 | @return promise {Promise} 36 | **/ 37 | findAll(store, typeClass, sinceToken) { 38 | let adapterResp = this.get('adapter').findAll(store, typeClass, sinceToken); 39 | store.set(`syncLoads.findAll.${typeClass.modelName}`, false); 40 | 41 | adapterResp.then(adapterPayload => { 42 | new Ember.RSVP.Promise(resolve => { 43 | store.pushPayload(typeClass.modelName, adapterPayload); 44 | return resolve(); 45 | }).then(() => { 46 | this.get('adapter').createOfflineJob('findAll', [store, typeClass, sinceToken, null, true], store); 47 | store.set(`syncLoads.findAll.${typeClass.modelName}`, true); 48 | }); 49 | }); 50 | 51 | return adapterResp; 52 | }, 53 | 54 | /** 55 | Saves the data received from the backend to the local storage. 56 | @method find 57 | @param store {DS.Store} 58 | @param typeClass {DS.Model} 59 | @param id {String|Number} 60 | @return promise {Promise} 61 | **/ 62 | find(store, typeClass, id) { 63 | let adapterResp = this.get('adapter').find(store, typeClass, id); 64 | store.set(`syncLoads.find.${typeClass.modelName}`, false); 65 | 66 | adapterResp.then(adapterPayload => { 67 | new Ember.RSVP.Promise(resolve => { 68 | store.pushPayload(typeClass.modelName, adapterPayload); 69 | return resolve(); 70 | }).then(() => { 71 | this.get('adapter').createOfflineJob('find', [store, typeClass, id, null, true], store); 72 | store.set(`syncLoads.find.${typeClass.modelName}`, true); 73 | }); 74 | 75 | }); 76 | 77 | return adapterResp; 78 | }, 79 | 80 | /** 81 | Saves the data received from the backend to the local storage. 82 | @method findQuery 83 | @param store {DS.Store} 84 | @param type {DS.Model} 85 | @param query {Model} 86 | @param promise {Promise} 87 | **/ 88 | findQuery(store, type, query) { 89 | let adapterResp = this.get('adapter').findQuery(store, type, query); 90 | store.set(`syncLoads.findQuery.${type.modelName}`, false); 91 | 92 | adapterResp.then(adapterPayload => { 93 | store.pushPayload(type.modelName, adapterPayload); 94 | store.set(`syncLoads.findQuery.${type.modelName}`, true); 95 | }); 96 | 97 | return adapterResp; 98 | }, 99 | /** 100 | Saves the data received from the backend to the local storage. 101 | @method findMany 102 | @param store {DS.Store} 103 | @param typeClass {DS.Model} 104 | @param ids {Array} 105 | @param snapshots {Array} 106 | **/ 107 | findMany(store, typeClass, ids, snapshots) { 108 | let adapterResp = this.get('adapter').findMany(store, typeClass, ids, snapshots); 109 | 110 | adapterResp.then(adapterPayload => { 111 | store.pushPayload(typeClass.modelName, adapterPayload); 112 | }); 113 | 114 | return adapterResp; 115 | }, 116 | /** 117 | Saves the data received from the backend to the local storage. 118 | @method createRecord 119 | @param store {DS.Store} 120 | @param type {DS.Model} 121 | @param snapshot {DS.Snapshot} 122 | @param fromJob {boolean} 123 | @return promise {Promise} 124 | **/ 125 | createRecord(store, type, snapshot, fromJob) { 126 | let adapter = this.get('adapter'); 127 | let apiHandler = handleApiErrors(function() { 128 | eraseOne(adapter.get('offlineAdapter'), store, type, snapshot); 129 | }); 130 | 131 | return adapter.createRecord(store, type, snapshot, fromJob) 132 | .then(result => { 133 | if (!adapter.get('skipCreateReplacing')) { 134 | eraseOne(adapter.get('offlineAdapter'), store, type, snapshot); 135 | store.pushPayload(type.modelName, result); 136 | } 137 | let recordId = extractTargetRecordFromPayload(store, type, result).id; 138 | persistOne(adapter.get('offlineAdapter'), store, type, recordId); 139 | 140 | return result; 141 | }) 142 | .catch(apiHandler); 143 | }, 144 | /** 145 | Saves the data received from the backend to the local storage. 146 | @method updateRecord 147 | @param store {DS.Store} 148 | @param type {DS.Model} 149 | @param snapshot {DS.Snapshot} 150 | @param fromJob {boolean} 151 | @return promise {Promise} 152 | **/ 153 | updateRecord(store, type, snapshot, fromJob) { 154 | let adapter = this.get('adapter'); 155 | return adapter.updateRecord(store, type, snapshot, fromJob); 156 | }, 157 | /** 158 | Removes the deleted record from the local storage. 159 | @method deleteRecord 160 | @param store {DS.Store} 161 | @param type {DS.Model} 162 | @param snapshot {DS.Snapshot} 163 | @param fromJob {boolean} 164 | @return promise {Promise} 165 | **/ 166 | deleteRecord(store, type, snapshot, fromJob) { 167 | let adapter = this.get('adapter'); 168 | return adapter.deleteRecord(store, type, snapshot, fromJob); 169 | }, 170 | }); 171 | -------------------------------------------------------------------------------- /addon/logics/sync-loads.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Object.extend({ 4 | findAll: Ember.Object.create({}), 5 | find: Ember.Object.create({}), 6 | findQuery: Ember.Object.create({}) 7 | }); 8 | -------------------------------------------------------------------------------- /addon/mixins/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module mixins 3 | **/ 4 | 5 | import Ember from 'ember'; 6 | import onlineJob from 'ember-data-offline/jobs/rest'; 7 | import offlineJob from 'ember-data-offline/jobs/localforage'; 8 | import moment from 'moment'; 9 | 10 | const { Mixin, $, on, computed, get, isPresent } = Ember; 11 | 12 | /** 13 | @class Base 14 | @constructor 15 | **/ 16 | export default Mixin.create({ 17 | /** 18 | Availability of the backend. 19 | @property isOffline 20 | @type {boolean} 21 | **/ 22 | isOffline: computed.not('isOnline'), 23 | /** 24 | @private 25 | @property onlineJob 26 | @type {Job} 27 | **/ 28 | onlineJob: onlineJob, 29 | /** 30 | @private 31 | @property offlineJob 32 | @type {Job} 33 | **/ 34 | offlineJob: offlineJob, 35 | /** 36 | Record cache expiration time. After expiration the record will be requested again 37 | rather than fetched from local storage. 38 | @property recordTTL 39 | @type {Object} 40 | **/ 41 | recordTTL: moment.duration(12, 'hours'), 42 | /** 43 | Property that shows if you need to replace your record from record, extracted from payload or not 44 | @property skipCreateReplacing 45 | @type Boolean 46 | **/ 47 | skipCreateReplacing: false, 48 | /** 49 | Used by adapter to get queue. 50 | 51 | Returns the syncronization job queue of an adapter or a store. 52 | @private 53 | @method _workingQueue 54 | @param store {DS.Store} 55 | @returns {Queue} 56 | **/ 57 | _workingQueue(store) { 58 | if (isPresent(get(this, 'EDOQueue'))) { 59 | return get(this, 'EDOQueue'); 60 | } else { 61 | return get(store, 'EDOQueue'); 62 | } 63 | }, 64 | 65 | /** 66 | Adds a job to the queue. If 'onDemandKey' param was passed, the job will be 67 | processed on-demand. 68 | 69 | @method addToQueue 70 | @param job {Job} job to add to queue. 71 | @param store {DS.Store} 72 | @param onDemandKey {String} 73 | **/ 74 | addToQueue(job, store, onDemandKey) { 75 | this._workingQueue(store).add(job, onDemandKey); 76 | }, 77 | 78 | /** 79 | Creates and adds an online job to the queue. If 'onDemandKey' param was passed, the job will be 80 | processed on-demand. 81 | 82 | @method createOnlineJob 83 | @param method {String} the name of method which will be runned by job. 84 | @param params {Array} 85 | @param onDemandKey {String} 86 | **/ 87 | createOnlineJob(method, params, onDemandKey) { 88 | let [store, typeClass] = params; 89 | let job = this.get('onlineJob').create({ 90 | adapter: store.lookupAdapter(typeClass.modelName) || this.get('onlineAdapter'), 91 | method: method, 92 | params: params, 93 | retryCount: 3, 94 | }); 95 | this.addToQueue(job, store, onDemandKey); 96 | }, 97 | /** 98 | Creates and adds an offline job to the queue. If 'onDemandKey' param was passed, the job will be 99 | processed on-demand. 100 | 101 | @method createOfflineJob 102 | @param method {String} the name of method which will be runned by job. 103 | @param params {Array} 104 | @param onDemandKey {String} 105 | **/ 106 | createOfflineJob(method, params, store) { 107 | let job = this.get('offlineJob').create({ 108 | adapter: this.get('offlineAdapter'), 109 | store: store, 110 | method: method, 111 | params: params, 112 | }); 113 | this.addToQueue(job, store); 114 | }, 115 | 116 | setup: on('init', function() { 117 | $(window).on('online', () => { 118 | this.set('isOnline', true); 119 | }); 120 | $(window).on('offline', () => { 121 | this.set('isOnline', false); 122 | }); 123 | this.set('isOnline', window.navigator.onLine); 124 | }), 125 | 126 | teardown: on('willDestroy', function() { 127 | $(window).off('online'); 128 | $(window).off('offline'); 129 | }), 130 | }); 131 | -------------------------------------------------------------------------------- /addon/mixins/job.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module mixins 3 | */ 4 | import Ember from 'ember'; 5 | const { computed, RSVP } = Ember; 6 | 7 | /** 8 | An abstract job. 9 | 10 | Examples: 11 | {{#crossLink "Localforage"}}{{/crossLink}} 12 | {{#crossLink "Rest"}}{{/crossLink}} 13 | 14 | @class Job 15 | @constructor 16 | **/ 17 | export default Ember.Mixin.create({ 18 | /** 19 | The number of retry attempts. 20 | @property retryCount 21 | @type {Number} 22 | **/ 23 | retryCount: 0, 24 | 25 | /** 26 | Shows if there are retry attempts left. 27 | @property needRetry 28 | @type {boolean} 29 | **/ 30 | needRetry: computed.gt('retryCount', 0), 31 | 32 | /** 33 | 34 | @method task 35 | **/ 36 | task() { 37 | return true; 38 | }, 39 | 40 | /** 41 | Called to perform the job. 42 | @method perform 43 | @return {Ember.Promise} 44 | **/ 45 | perform() { 46 | return RSVP.Promise.resolve().then(() => { 47 | return this.task(); 48 | }); 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /addon/mixins/localstorage-id.js: -------------------------------------------------------------------------------- 1 | /*WE DON'T NEED THIS ANYMORE remove after remove form rc*/ 2 | /** 3 | @module mixins 4 | **/ 5 | import Ember from 'ember'; 6 | 7 | /** 8 | This class will be removed soon. 9 | @class LocalstorageId 10 | @deprecated 11 | **/ 12 | export default Ember.Mixin.create({ 13 | serialize() { 14 | let json = this._super.apply(this, arguments); 15 | if (json._id) { 16 | json.id = json._id; 17 | } 18 | return json; 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /addon/mixins/offline.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module mixins 3 | **/ 4 | import Ember from 'ember'; 5 | import baseMixin from 'ember-data-offline/mixins/base'; 6 | import debug from 'ember-data-offline/utils/debug'; 7 | import extractTargetRecordFromPayload from 'ember-data-offline/utils/extract-online'; 8 | import { isExpiredOne, isExpiredMany, isExpiredAll } from 'ember-data-offline/utils/expired'; 9 | import { updateMeta } from 'ember-data-offline/utils/meta'; 10 | 11 | /** 12 | Offline mixin redefines all adapter persistance methods to make request to offline storage. 13 | 14 | @class Offline 15 | @extends Ember.Mixin 16 | @uses Base 17 | @constructor 18 | **/ 19 | export default Ember.Mixin.create(baseMixin, { 20 | shouldReloadAll() { 21 | return false; 22 | }, 23 | shouldBackgroundReloadAll: function() { 24 | return true; 25 | }, 26 | shouldReloadRecord() { 27 | return false; 28 | }, 29 | shouldBackgroundReloadRecord() { 30 | return true; 31 | }, 32 | 33 | /** 34 | Returns the metadata for a given model. Returned metadata contains information about 35 | latest fetch and update times. 36 | 37 | @method metadataForType 38 | @param typeClass {DS.Model} 39 | @returns metadata {Object} 40 | **/ 41 | metadataForType(typeClass) { 42 | return this._namespaceForType(typeClass).then(namespace => { 43 | return namespace["__data_offline_meta__"]; 44 | }); 45 | }, 46 | 47 | /** 48 | Overrides the method of an extended adapter (offline). Fetches a JSON array 49 | for all of the records for a given type from offline adapter. If fetched 50 | records are expired, it tries to create an online job to fetch the records 51 | from the online adapter and save them locally. 52 | 53 | @method findAll 54 | @param store {DS.Store} 55 | @param typeClass {DS.Model} 56 | @param sinceToken {String} 57 | @param snapshots {Array} 58 | @param fromJob {boolean} 59 | @return promise {Promise} 60 | **/ 61 | findAll: function(store, typeClass, sinceToken, snapshots, fromJob) { 62 | debug('findAll offline', typeClass.modelName); 63 | return this._super.apply(this, arguments).then(records => { 64 | if (!fromJob) { 65 | //TODO find way to pass force reload option here 66 | this.metadataForType(typeClass).then(meta => { 67 | if (isExpiredAll(store, typeClass, meta)) { 68 | this.createOnlineJob('findAll', [store, typeClass, sinceToken, snapshots, true]); 69 | } 70 | }); 71 | } 72 | return records; 73 | }); 74 | }, 75 | 76 | /** 77 | Overrides the method of an extended adapter (offline). Fetches a JSON array for all of the records for a given type 78 | from offline adapter. If fetched expired records and then tries to create an online job to fetch the records 79 | from the online adapter and save them locally. 80 | @method find 81 | @param store {DS.Store} 82 | @param typeClass {DS.Model} 83 | @param id {String|Number} 84 | @param snapshot {DS.Snapshot} 85 | @param fromJob {boolean} 86 | @return promise {Promise} 87 | **/ 88 | find: function(store, typeClass, id, snapshot, fromJob) { 89 | return this._super.apply(this, arguments).then(record => { 90 | if (!fromJob) { 91 | if (isExpiredOne(store, typeClass, record) && !Ember.isEmpty(id)) { 92 | this.createOnlineJob('find', [store, typeClass, id, snapshot, true]); 93 | } 94 | } 95 | if (Ember.isEmpty(record) && !Ember.isEmpty(id)) { 96 | let primaryKey = store.serializerFor(typeClass.modelName).primaryKey; 97 | let stub = {}; 98 | stub[primaryKey] = id; 99 | return stub; 100 | } 101 | return record; 102 | }); 103 | }, 104 | 105 | /** 106 | Overrides the method of an extended adapter (offline). Fetches a JSON array for all of the records for a given type 107 | from offline adapter. If fetched expired records and then tries to create an online job to fetch the records 108 | from the online adapter and save them locally. 109 | 110 | @method query 111 | @param store {DS.Store} 112 | @param typeClass {DS.Model} 113 | @param query {Object} 114 | @param recordArray {Array} 115 | @param fromJob {boolean} 116 | @return promise {Promise} 117 | **/ 118 | query: function(store, typeClass, query, recordArray, fromJob) { 119 | return this._super.apply(this, arguments).then(records => { 120 | //TODO think how to remove this dirty hasck 121 | if (Ember.isEmpty(records)) { 122 | return this.get('onlineAdapter').findQuery(store, typeClass, query, recordArray, fromJob).then(onlineRecords => { 123 | return extractTargetRecordFromPayload(store, typeClass, onlineRecords); 124 | }); 125 | } 126 | else { 127 | if (!fromJob) { 128 | this.createOnlineJob('query', [store, typeClass, query, recordArray, true]); 129 | } 130 | } 131 | return records; 132 | }); 133 | }, 134 | 135 | /** 136 | Overrides the method of an extended adapter (offline). Fetches a JSON array for all of the records for a given type 137 | from offline adapter. If fetched expired records and then tries to create an online job to fetch the records 138 | from the online adapter and save them locally. 139 | 140 | @method findMany 141 | @param store {DS.Store} 142 | @param typeClass {DS.Model} 143 | @param ids {Array} 144 | @param snapshots {Array} 145 | @param fromJob {boolean} 146 | @return promise {Promise} 147 | **/ 148 | findMany: function(store, typeClass, ids, snapshots, fromJob) { 149 | // debug('findMany offline', type.modelName); 150 | return this._super.apply(this, arguments).then(records => { 151 | if (!fromJob) { 152 | if (isExpiredMany(store, typeClass, records) && !Ember.isEmpty(ids)) { 153 | this.createOnlineJob('findMany', [store, typeClass, ids, snapshots, true]); 154 | } 155 | } 156 | if (Ember.isEmpty(records) && !Ember.isEmpty(ids)) { 157 | let primaryKey = store.serializerFor(typeClass.modelName).primaryKey; 158 | return ids.map(id => { 159 | let stub = {}; 160 | stub[primaryKey] = id; 161 | return stub; 162 | }); 163 | } 164 | return records; 165 | }); 166 | }, 167 | 168 | /** 169 | Overrides the method of an extended adapter (offline). 170 | If tries to create an online job to create the record and save it locally. 171 | @method createRecord 172 | @param store {DS.Store} 173 | @param type {DS.Model} 174 | @param snapshot {DS.Snapshot} 175 | @param fromJob {boolean} 176 | @return promise {Promise} 177 | **/ 178 | createRecord(store, type, snapshot, fromJob) { 179 | updateMeta(snapshot); 180 | 181 | if (!fromJob) { 182 | if (this.get('isOnline')) { 183 | this.createOnlineJob('createRecord', [store, type, snapshot, true], `create$${type.modelName}`); 184 | } 185 | else { 186 | this.createOnlineJob('createRecord', [store, type, snapshot, true]); 187 | } 188 | } 189 | 190 | return this._super.apply(this, [store, type, snapshot]); 191 | }, 192 | /** 193 | Overrides the method of an extended adapter (offline). 194 | If tries to create an online job to update the record and save it locally. 195 | @method updateRecord 196 | @param store {DS.Store} 197 | @param type {DS.Model} 198 | @param snapshot {DS.Snapshot} 199 | @param fromJob {boolean} 200 | @return promise {Promise} 201 | **/ 202 | updateRecord(store, type, snapshot, fromJob) { 203 | if (!fromJob) { 204 | this.createOnlineJob('updateRecord', [store, type, snapshot, true]); 205 | } 206 | 207 | updateMeta(snapshot); 208 | return this._super.apply(this, [store, type, snapshot]); 209 | }, 210 | /** 211 | Overrides the method of an extended adapter (offline). 212 | If tries to create an online job to delete the record and remove it locally. 213 | @method deleteRecord 214 | @param store {DS.Store} 215 | @param type {DS.Model} 216 | @param snapshot {DS.Snapshot} 217 | @param fromJob {boolean} 218 | @return promise {Promise} 219 | **/ 220 | deleteRecord(store, type, snapshot, fromJob) { 221 | if (!fromJob) { 222 | this.createOnlineJob('deleteRecord', [store, type, snapshot, true]); 223 | } 224 | return this._super.apply(this, arguments); 225 | } 226 | }); 227 | -------------------------------------------------------------------------------- /addon/mixins/online.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module mixins 3 | **/ 4 | import Ember from 'ember'; 5 | import baseMixin from 'ember-data-offline/mixins/base'; 6 | import debug from 'ember-data-offline/utils/debug'; 7 | 8 | 9 | /** 10 | Online mixin redefines all adapter persistance methods to make request to online storage. 11 | 12 | @class Online 13 | @extends Ember.Mixin 14 | @uses Base 15 | @constructor 16 | **/ 17 | export default Ember.Mixin.create(baseMixin, { 18 | /** 19 | Fetches a JSON array for all of the records for a given type from online adapter. 20 | @method findAll 21 | @param store {DS.Store} 22 | @param typeClass {DS.Model} 23 | @return promise {Promise} 24 | **/ 25 | findAll: function(store, typeClass) { 26 | debug('findAll online', typeClass.modelName); 27 | return this._super.apply(this, arguments); 28 | }, 29 | /** 30 | Fetches a JSON array for all of the records for a given type from online adapter. 31 | @method find 32 | @return promise {Promise} 33 | **/ 34 | find: function() { 35 | return this._super.apply(this, arguments); 36 | }, 37 | /** 38 | Fetches a JSON array for all of the records for a given type from online adapter. 39 | @method findQuery 40 | @param store {DS.Store} 41 | @param type {DS.Model} 42 | @param query {Object} 43 | @param recordArray {Array} 44 | @param fromJob {Array} 45 | @return promise {Promise} 46 | **/ 47 | findQuery: function(store, type, query, recordArray, fromJob) { 48 | let onlineResp = this._super.apply(this, arguments); 49 | return onlineResp.then(resp => { 50 | if (!fromJob && store.get('isOfflineEnabled')) { 51 | this.createOfflineJob('findQuery', [store, type, query, resp, true], store); 52 | } 53 | return resp; 54 | }); 55 | }, 56 | /** 57 | Fetches a JSON array for all of the records for a given type from online adapter. 58 | @method findMany 59 | @param store {DS.Store} 60 | @param typeClass {DS.Model} 61 | @param ids {Array} 62 | @param snapshots {Array} 63 | @param fromJob {Array} 64 | @return promise {Promise} 65 | **/ 66 | findMany: function(store, typeClass, ids, snapshots, fromJob) { 67 | //TODO add some config param for such behavior 68 | let onlineResp = this.findAll(store, typeClass, null, true); 69 | 70 | return onlineResp.then(resp => { 71 | if (!fromJob && store.get('isOfflineEnabled')) { 72 | this.createOfflineJob('findMany', [store, typeClass, ids], store); 73 | } 74 | return resp; 75 | }); 76 | }, 77 | /** 78 | Saves the record via the parent offline adapter. 79 | @method createRecord 80 | @return promise {Promise} 81 | **/ 82 | createRecord() { 83 | return this._super.apply(this, arguments); 84 | }, 85 | /** 86 | Updates the record via the parent offline adapter. 87 | @method updateRecord 88 | @return promise {Promise} 89 | **/ 90 | updateRecord() { 91 | return this._super.apply(this, arguments); 92 | }, 93 | 94 | /** 95 | Deletes the record via the parent offline adapter. 96 | @method deleteRecord 97 | @return promise {Promise} 98 | **/ 99 | deleteRecord() { 100 | return this._super.apply(this, arguments); 101 | } 102 | }); 103 | -------------------------------------------------------------------------------- /addon/queue.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module ember-data-offline 3 | */ 4 | import Ember from 'ember'; 5 | /** 6 | A queue of syncronization jobs. 7 | 8 | @class Queue 9 | @constructor 10 | @extends Ember.Object 11 | **/ 12 | export default Ember.Object.extend({ 13 | /** 14 | @property name 15 | @type {String} 16 | **/ 17 | name: 'normal', 18 | /** 19 | Interval between job retries. 20 | @property retryOnFailureDelay 21 | @type {Number} 22 | **/ 23 | retryOnFailureDelay: 5000, 24 | /** 25 | Job execution delay. 26 | @property delay 27 | @type {Number} 28 | **/ 29 | delay: 500, 30 | /** 31 | Number of workers in a queue. 32 | @property workers 33 | @type {Number} 34 | **/ 35 | workers: 5, 36 | /** 37 | An array of active jobs in a queue. 38 | @property activeJobs 39 | @type {Ember.Array} 40 | **/ 41 | activeJobs: null, 42 | /** 43 | An array of pending jobs in a queue. 44 | @property pendingJobs 45 | @type {Ember.Array} 46 | **/ 47 | pendingJobs: null, 48 | /** 49 | An array of failed jobs in a queue. 50 | @property failureJobs 51 | @type {Ember.Array} 52 | **/ 53 | failureJobs: null, 54 | /** 55 | An array of retried jobs in a queue. 56 | @property retryJobs 57 | @type {Ember.Array} 58 | **/ 59 | retryJobs: null, 60 | /** 61 | An array of on-demand jobs. 62 | @property onDemandJobs 63 | @type {Ember.Object} 64 | **/ 65 | onDemandJobs: null, 66 | 67 | init: function() { 68 | this.setProperties({ 69 | activeJobs: Ember.A(), 70 | pendingJobs: Ember.A(), 71 | failureJobs: Ember.A(), 72 | retryJobs: Ember.A(), 73 | onDemandJobs: Ember.Object.create(), 74 | }); 75 | this._super.apply(this, arguments); 76 | }, 77 | 78 | /** 79 | @method runJob 80 | @param Job {Job} 81 | **/ 82 | runJob(job) { 83 | //TODO Think about to build in queue in ember(ed) runloop 84 | this.get('activeJobs').pushObject(job); 85 | let delay = job.get('adapter.throttle') || job.get('delay') || this.get('delay'); 86 | Ember.run.later(() => { 87 | this.process(job); 88 | }, delay); 89 | }, 90 | /** 91 | Checks if the job exists in a queue. 92 | @method isJobExist 93 | @param Job {Job} 94 | @return {boolean} 95 | **/ 96 | isJobExist(job) { 97 | let pendingJob = this.get('pendingJobs').find((item) => { 98 | return item.get('adapter') === job.get('adapter') && item.get('method') === job.get('method'); 99 | }); 100 | let retryJob = this.get('retryJobs').find((item) => { 101 | return item.get('adapter') === job.get('adapter') && item.get('method') === job.get('method'); 102 | }); 103 | return pendingJob || retryJob; 104 | }, 105 | 106 | pendingJobObserver: Ember.observer('pendingJobs.[]','activeJobs.[]', function() { 107 | if (this.get('pendingJobs.length') <= 0) { 108 | return; 109 | } 110 | if (this.get('activeJobs').length < this.get('workers')) { 111 | let job = this.get('pendingJobs').shift(); 112 | if (job) { 113 | this.runJob(job); 114 | } 115 | } 116 | }), 117 | 118 | /** 119 | Adds a job to the queue. If 'onDemandKey' param was passed, the job will be processed on demand. 120 | 121 | @method add 122 | @param Job {Job} job to add 123 | @param onDemandKey {String} 124 | @return {Job} the passed Job 125 | **/ 126 | add: function(job, onDemandKey) { 127 | if (!Ember.isEmpty(onDemandKey)) { 128 | return this.set(`onDemandJobs.${onDemandKey}`, job); 129 | } 130 | if (!this.isJobExist(job)) { 131 | return this.get('pendingJobs').pushObject(job); 132 | } 133 | }, 134 | 135 | /** 136 | Removes the job from the queue. 137 | 138 | @method remove 139 | @param Job {Job} job to remove 140 | **/ 141 | remove: function(job) { 142 | this.get('pendingJobs').removeObject(job); 143 | }, 144 | 145 | /** 146 | Performs the job. After successful processing removes the job from lists of active and retried jobs. 147 | If a job fails, it will be automatically retried with a delay, until it succeeds or runs out of retries. 148 | 149 | @method process 150 | @param Job {Job} job to process 151 | **/ 152 | process: function(job) { 153 | let queue = this; 154 | job.perform().then(() => { 155 | this.get('activeJobs').removeObject(job); 156 | this.get('retryJobs').removeObject(job); 157 | }, () => { 158 | 159 | this.get('activeJobs').removeObject(job); 160 | queue.get('retryJobs').removeObject(job); 161 | 162 | if (job.get('needRetry')) { 163 | job.decrementProperty('retryCount'); 164 | queue.get('retryJobs').pushObject(job); 165 | Ember.run.later(() => { 166 | queue.process(job); 167 | }, job.get('retryDelay') || queue.get('retryOnFailureDelay')); 168 | } else { 169 | queue.get('retryJobs').removeObject(job); 170 | queue.get('failureJobs').pushObject(job); 171 | } 172 | }); 173 | } 174 | }); 175 | -------------------------------------------------------------------------------- /addon/request.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import baseMixin from 'ember-data-offline/mixins/base'; 3 | import jobMixin from 'ember-data-offline/mixins/job'; 4 | import ajaxJob from 'ember-data-offline/jobs/ajax'; 5 | 6 | export default Ember.Object.extend(baseMixin, { 7 | store: Ember.inject.service(), 8 | retryCount: 60, 9 | retryDelay: 30000, 10 | 11 | exec(opts, syncs) { 12 | let params = Ember.merge(this._defaultParams(), opts); 13 | 14 | if (this.get('isOnline')) { 15 | return Ember.$.ajax(params); 16 | } 17 | 18 | return this._offlineScenario(params, syncs); 19 | }, 20 | 21 | _offlineScenario(params, syncs) { 22 | let store = this.get('store'); 23 | 24 | let job = ajaxJob.create({ 25 | retryCount: this.get('retryCount'), 26 | retryDelay: this.get('retryDelay'), 27 | params: params 28 | }); 29 | store.EDOQueue.add(job); 30 | 31 | if (syncs && typeof syncs === 'function') { 32 | let job = Ember.Object.extend(jobMixin).create({ 33 | delay: 1, 34 | task: syncs 35 | }); 36 | store.EDOQueue.add(job); 37 | } 38 | 39 | return Ember.RSVP.Promise.resolve(); 40 | }, 41 | 42 | _defaultParams() { 43 | let defaults = { 44 | type: "GET" 45 | }; 46 | 47 | return Ember.merge(defaults, this.sessionParams()); 48 | }, 49 | 50 | sessionParams() { 51 | return {}; 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /addon/utils/debug.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { debug } = Ember.Logger; 3 | 4 | //Later we can remove this, but until stable release we need it 5 | export default function() { 6 | debug('[ember-data-offline]:', Array.prototype.slice.call(arguments, 0).toString()); 7 | } 8 | -------------------------------------------------------------------------------- /addon/utils/erase-offline.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module utils 3 | @class EraseOne 4 | **/ 5 | 6 | /** 7 | Erases the given record (snapshot) from store. 8 | 9 | @method eraseOne 10 | @param adapter {DS.Adapter} 11 | @param store {DS.Store} 12 | @param type {DS.Model} 13 | @param snapshot {DS.Snapshot} 14 | **/ 15 | var eraseOne = function(adapter, store, type, snapshot) { 16 | let recordToDelete = store.peekRecord(type.modelName, snapshot.id); 17 | store.deleteRecord(recordToDelete); 18 | adapter.deleteRecord(store, type, snapshot, true); 19 | }; 20 | var eraseAll = function(adapter, store, type) { 21 | store.unloadAll(type.modelName); 22 | adapter.queue.attach((resolve, reject) => { 23 | adapter._namespaceForType(type).then(() => { 24 | adapter.persistData(type, []).then(() => { 25 | resolve(); 26 | }, (err) => { 27 | reject(err); 28 | }); 29 | }, (err) => { 30 | reject(err); 31 | }); 32 | }); 33 | }; 34 | 35 | export { eraseOne, eraseAll }; 36 | export default eraseOne; 37 | -------------------------------------------------------------------------------- /addon/utils/expired.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module utils 3 | @class Expired 4 | 5 | **/ 6 | import Ember from 'ember'; 7 | import moment from 'moment'; 8 | 9 | /** 10 | Checks if the record is expired by comparing last fetch time with recordTTL. 11 | 12 | @private 13 | @method _isExpired 14 | @param record {Object} 15 | @param RecordTTl {Number} 16 | @return {boolean} 17 | **/ 18 | var _isExpired = function(record, recordTTL) { 19 | if (!record) { 20 | return true; 21 | } 22 | let updatedAt = record["__data_offline_meta__"] ? record['__data_offline_meta__'].fetchedAt : record.fetchedAt; 23 | //this is for locally created records 24 | if (!updatedAt) { 25 | return true; 26 | } 27 | if (moment().diff(updatedAt) > recordTTL) { 28 | return true; 29 | } 30 | return false; 31 | }; 32 | /** 33 | Checks if the record is expired by comparing last fetch time with recordTTL. The information about record ttl 34 | is received from the adapter for a given type. 35 | 36 | @method isExpiredOne 37 | @param store {DS.Store} 38 | @param typeClass {DS.Model} 39 | @param record {} 40 | @return {boolean} 41 | **/ 42 | var isExpiredOne = function(store, typeClass, record) { 43 | if (Ember.isEmpty(record)) { 44 | return true; 45 | } 46 | let recordTTL = store.lookupAdapter(typeClass.modelName).get('recordTTL'); 47 | 48 | return _isExpired(record, recordTTL); 49 | }; 50 | 51 | /** 52 | Checks if the collection of records is expired by comparing last fetch time with recordTTL. Information about the record (collection) 53 | ttl is received from the adapter for a given type. 54 | 55 | @method isExpiredAll 56 | @param store {DS.Store} 57 | @param typeClass {DS.Model} 58 | @param meta {Object} 59 | @return {boolean} 60 | **/ 61 | var isExpiredAll = function(store, typeClass, meta) { 62 | let adapter = store.lookupAdapter(typeClass.modelName); 63 | let ttl = adapter.get('collectionTTL') || adapter.get('recordTTL'); 64 | 65 | return _isExpired(meta, ttl); 66 | }; 67 | 68 | 69 | /** 70 | Checks if any record from an array of records is expired by comparing last fetch time with recordTTL. Information about record (collection) 71 | ttl is received from the adapter for a given type. 72 | 73 | @method isExpiredMany 74 | @param store {DS.Store} 75 | @param typeClass {DS.Model} 76 | @param records {Array} 77 | @return {boolean} 78 | **/ 79 | var isExpiredMany = function(store, typeClass, records) { 80 | if (Ember.isEmpty(records)) { 81 | return true; 82 | } 83 | let adapter = store.lookupAdapter(typeClass.modelName); 84 | let recordTTL = adapter.get('recordTTL'); 85 | 86 | return records.reduce((p, record) => { 87 | return p || _isExpired(record, recordTTL); 88 | }, false); 89 | }; 90 | 91 | export { isExpiredOne, isExpiredMany, isExpiredAll }; 92 | -------------------------------------------------------------------------------- /addon/utils/extract-online.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module utils 3 | @class ExtractOnline 4 | **/ 5 | 6 | /** 7 | Extracts the record from the payload of your backend. 8 | 9 | @method extractTargetRecordFromPayload 10 | @param store {DS.Store} 11 | @param typeClass {DS.Model} 12 | @param recordToExtractFrom {Object} payload from which the record will be extracted 13 | @return extracted target {Object} 14 | **/ 15 | var extractTargetRecordFromPayload = function extractTargetRecordFromPayload(store, typeClass, recordToExtractFrom) { 16 | let modelName = typeClass.modelName; 17 | let serializer = store.serializerFor(modelName); 18 | let payload = serializer.normalizePayload(recordToExtractFrom); 19 | let modelNameInPayload = Object.keys(payload).filter(key => { 20 | return serializer.modelNameFromPayloadKey(key) === modelName; 21 | })[0]; 22 | if (!modelNameInPayload) { 23 | return null; 24 | } 25 | return payload[modelNameInPayload]; 26 | }; 27 | 28 | export { extractTargetRecordFromPayload }; 29 | export default extractTargetRecordFromPayload; 30 | -------------------------------------------------------------------------------- /addon/utils/handle-api-errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module utils 3 | @class HandleApiErrors 4 | **/ 5 | import Ember from 'ember'; 6 | 7 | /** 8 | Handles server response errors. 9 | @method 10 | @param callback {function} 11 | @return promise {Promise} 12 | **/ 13 | export default function(callback) { 14 | //TODO: 422 behavior 15 | return function(err) { 16 | if (err && !Ember.isEmpty(err.errors)) { 17 | if (err.errors[0].status === "408") { 18 | return Ember.RSVP.resolve(); 19 | } 20 | else { 21 | callback(); 22 | return Ember.RSVP.reject(); 23 | } 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /addon/utils/is-object-empty.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module utils 3 | @class IsObjectEmpty 4 | **/ 5 | 6 | /** 7 | Checks if object is empty. 8 | @method isObjectEmpty 9 | @param obj {Object} 10 | @return {boolean} 11 | **/ 12 | export default function(obj) { 13 | return Object.keys(obj).length === 0; 14 | } 15 | -------------------------------------------------------------------------------- /addon/utils/meta.js: -------------------------------------------------------------------------------- 1 | /** 2 | Methods for manipulations with record metadata (fetch and update timestamps). 3 | @module utils 4 | @class Meta 5 | **/ 6 | import Ember from 'ember'; 7 | 8 | /** 9 | Adds the metadata to the given snapshot. 10 | @method addMeta 11 | @param snapshot {DS.snapshot} 12 | @param infoToAdd {Object} 13 | **/ 14 | var addMeta = function addMeta(snapshot, infoToAdd) { 15 | //TODO add store meta changing here too 16 | snapshot.record.set('__data_offline_meta__', Ember.merge(snapshot.record.get('__data_offline_meta__') || {}, infoToAdd)); 17 | }; 18 | /** 19 | Adds the 'updatedAt' field to the metadata of a given snapshot. The 'fetchedAt' property stores the information 20 | about the last update time of a record and is used for expirations checks. 21 | @method addUpdatedAtToMeta 22 | @param snapshot {DS.snapshot} 23 | **/ 24 | var addUpdatedAtToMeta = function addUpdatedAtToMeta(snapshot) { 25 | addMeta(snapshot, { 26 | updatedAt: new Date().toString() 27 | }); 28 | }; 29 | /** 30 | Adds the 'fetchedAt' field to the metadata of a given snapshot. The 'fetchedAt' property stores the information 31 | about the last fetch time of record and is used for expiration checks. 32 | @method addFetchedAtToMeta 33 | @param snapshot {DS.snapshot} 34 | **/ 35 | var addFetchedAtToMeta = function addFetchedAtToMeta(snapshot, fetchedAt) { 36 | let date = fetchedAt || new Date().toString(); 37 | addMeta(snapshot, { 38 | fetchedAt: date 39 | }); 40 | }; 41 | /** 42 | Updates the last fetch and update timestamps in the metadata. 43 | @method updateMeta 44 | @param snapshot {DS.snapshot} 45 | **/ 46 | var updateMeta = function updateMeta(snapshot) { 47 | //TODO maybe updatedAt not always gets setted? 48 | let store = snapshot.record.store; 49 | let modelName = snapshot._internalModel.modelName; 50 | let storeMetadata = store.metadataFor(modelName); 51 | 52 | addUpdatedAtToMeta(snapshot); 53 | addFetchedAtToMeta(snapshot, Ember.getWithDefault(storeMetadata, `__data_offline_meta__.${snapshot.id}.fetchedAt`, null)); 54 | }; 55 | 56 | export { addMeta, addUpdatedAtToMeta, addFetchedAtToMeta, updateMeta }; 57 | 58 | export default addMeta; 59 | -------------------------------------------------------------------------------- /addon/utils/persist-offline.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module utils 3 | @class PersistOffline 4 | **/ 5 | import Ember from 'ember'; 6 | import extractTargetRecordFromPayload from 'ember-data-offline/utils/extract-online'; 7 | import { updateMeta } from 'ember-data-offline/utils/meta'; 8 | 9 | /** 10 | @private 11 | @method _persistArray 12 | @param array {Array} 13 | @param adaper {DS.Adapter} 14 | @param typelClass {DS.Model} 15 | @param withMeta {boolean} 16 | **/ 17 | var _persistArray = function(array, adapter, typeClass, withMeta) { 18 | let serializer = adapter.serializer; 19 | adapter.queue.attach((resolve, reject) => { 20 | adapter._namespaceForType(typeClass).then(namespace => { 21 | if (!Ember.isEmpty(array)) { 22 | for (var i = 0, len = array.length; i !== len; i++) { 23 | let snapshot = array[i]._createSnapshot(); 24 | updateMeta(snapshot); 25 | 26 | let recordHash = serializer.serialize(snapshot, {includeId: true}); 27 | namespace.records[recordHash.id] = recordHash; 28 | } 29 | } 30 | if (withMeta) { 31 | namespace["__data_offline_meta__"] = { 32 | fetchedAt: new Date().toString() 33 | }; 34 | } 35 | adapter.persistData(typeClass, namespace).then(() => { 36 | resolve(); 37 | }, (err) => { 38 | reject(err); 39 | }); 40 | }, (err) => { 41 | reject(err); 42 | }); 43 | }); 44 | }; 45 | /** 46 | @method persistOne 47 | @param adaper {DS.Adapter} 48 | @param store {DS.Store} 49 | @param typelClass {DS.Model} 50 | @param id {String|Number} 51 | **/ 52 | var persistOne = function persistOne(adapter, store, typeClass, id) { 53 | let modelName = typeClass.modelName; 54 | let recordFromStore = store.peekRecord(modelName, id); 55 | if (Ember.isEmpty(recordFromStore)) { 56 | return; 57 | } 58 | let snapshot = recordFromStore._createSnapshot(); 59 | 60 | return adapter.createRecord(store, typeClass, snapshot, true); 61 | }; 62 | 63 | /** 64 | @method persistAll 65 | @param adaper {DS.Adapter} 66 | @param store {DS.Store} 67 | @param typelClass {DS.Model} 68 | **/ 69 | var persistAll = function persistAll(adapter, store, typeClass) { 70 | let fromStore = store.peekAll(typeClass.modelName).toArray(); 71 | _persistArray(fromStore, adapter, typeClass, true); 72 | }; 73 | /** 74 | @method persistMany 75 | @param adaper {DS.Adapter} 76 | @param store {DS.Store} 77 | @param typelClass {DS.Model} 78 | **/ 79 | var persistMany = function persistMany(adapter, store, typeClass) { 80 | //While we using findAll instead of findMany we better use this for persistance 81 | let fromStore = store.peekAll(typeClass.modelName).toArray(); 82 | _persistArray(fromStore, adapter, typeClass); 83 | }; 84 | /** 85 | @deprecated 86 | @method persistQuery 87 | @param adaper {DS.Adapter} 88 | @param store {DS.Store} 89 | @param typelClass {DS.Model} 90 | @param onlineResp {Promise} 91 | **/ 92 | var persistQuery = function persistQuery(adapter, store, typeClass, onlineResp) { 93 | let fromStore = store.peekAll(typeClass.modelName); 94 | if (Ember.isEmpty(fromStore)) { 95 | return; 96 | } 97 | let onlineIds = extractTargetRecordFromPayload(onlineResp).map(record => record.id); 98 | fromStore.forEach(record => { 99 | if (onlineIds.indexOf(record.id) > -1) { 100 | let snapshot = record._createSnapshot(); 101 | adapter.createRecord(store, typeClass, snapshot, true); 102 | } 103 | }); 104 | }; 105 | 106 | export { persistOne, persistAll, persistMany, persistQuery }; 107 | /** 108 | @method persistOffline 109 | @param adaper {DS.Adapter} 110 | @param store {DS.Store} 111 | @param typelClass {DS.Model} 112 | @param onlineResp {Promise} 113 | @param method {String} the name of method whch will be executed. 114 | **/ 115 | export default function persistOffline(adapter, store, typeClass, onlineResp, method) { 116 | if (method === 'find') { 117 | persistOne(adapter, store, typeClass, onlineResp); 118 | } else if (method === 'findMany') { 119 | persistMany(adapter, store, typeClass, onlineResp); 120 | } else if (method === 'findQuery') { 121 | persistQuery(adapter, store, typeClass, onlineResp); 122 | } else { 123 | persistAll(adapter, store, typeClass); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/api-hogs/ember-data-offline/920597742e88d6961ce7cf34332d308a458dc533/app/.gitkeep -------------------------------------------------------------------------------- /app/blueprints/ember-data-offline.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-data-offline/blueprints/ember-data-offline'; -------------------------------------------------------------------------------- /app/initializers/edo-request.js: -------------------------------------------------------------------------------- 1 | import request from 'ember-data-offline/request'; 2 | 3 | export function initialize(container, app) { 4 | container.register('ember-data-offline:request', request); 5 | 6 | app.inject('EDORequest', 'store', 'service:store'); 7 | app.inject('route', 'EDORequest', 'ember-data-offline:request'); 8 | app.inject('controller', 'EDORequest', 'ember-data-offline:request'); 9 | app.inject('model', 'EDORequest', 'ember-data-offline:request'); 10 | app.inject('component', 'EDORequest', 'ember-data-offline:request'); 11 | }; 12 | 13 | export default { 14 | name: 'edo-request', 15 | initialize: initialize 16 | }; 17 | -------------------------------------------------------------------------------- /app/services/store.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import DS from 'ember-data'; 3 | import syncLoads from 'ember-data-offline/logics/sync-loads'; 4 | import Queue from 'ember-data-offline/queue'; 5 | import Config from 'ember-data-offline/config'; 6 | import config from '../config/environment'; 7 | import eraseOne from 'ember-data-offline/utils/erase-offline'; 8 | 9 | var mainConfig = Config.create({ 10 | custom: Ember.getWithDefault(config, 'ember-data-offline', {}) 11 | }); 12 | 13 | DS.Model.reopen({ 14 | save() { 15 | if (!mainConfig.get('isEnabled')) { 16 | return this._super.apply(this, arguments); 17 | } 18 | 19 | let modelName = this._internalModel.modelName; 20 | let store = this.store; 21 | return this._super.apply(this, arguments).then(resp => { 22 | let job = store.get(`EDOQueue.onDemandJobs.create$${modelName}`); 23 | if (!job) { 24 | return resp; 25 | } 26 | return job.perform(); 27 | }) 28 | .then(result => { 29 | return result; 30 | }); 31 | } 32 | }); 33 | 34 | export default DS.Store.extend({ 35 | syncLoads: syncLoads.create(), 36 | EDOQueue: Queue.create(), 37 | isOfflineEnabled: mainConfig.get('isEnabled'), 38 | forceFetchAll(modelName) { 39 | this.adapterFor(modelName).createOnlineJob('findAll', [this, this.modelFor(modelName)]); 40 | return this.peekAll(modelName); 41 | }, 42 | forceFetchRecord(modelName, id) { 43 | this.adapterFor(modelName).createOnlineJob('find', [this, this.modelFor(modelName), id]); 44 | return this.peekRecord(modelName, id); 45 | }, 46 | eraseRecord(record) { 47 | let modelName = record._internalModel.modelName; 48 | return eraseOne(this.adapterFor(modelName), this, this.modelFor(modelName), record._createSnapshot()); 49 | }, 50 | syncRecord(record) { 51 | this.eraseRecord(record); 52 | this.forceFetchAll(record._internalModel.modelName); 53 | }, 54 | 55 | adapterFor() { 56 | if (!mainConfig.get('isEnabled')) { 57 | return this._super.apply(this, arguments); 58 | } 59 | return this._super.apply(this, arguments).get('offlineAdapter') || this._super.apply(this, arguments); 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /blueprints/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "console" 4 | ], 5 | "strict": false 6 | } 7 | -------------------------------------------------------------------------------- /blueprints/ember-data-offline/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: 'main', 3 | normalizeEntityName: function() {}, 4 | afterInstall: function() { 5 | var _this = this; 6 | return this.addAddonToProject({ 7 | name: "ember-moment", 8 | target: "2.0.1" 9 | }).then(function() { 10 | return _this.addAddonToProject({ 11 | name: "ember-localforage-adapter", 12 | target: "git+https://github.com/igorrKurr/ember-localforage-adapter.git" 13 | }); 14 | }); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-data-offline", 3 | "dependencies": { 4 | "ember": "1.13.5", 5 | "ember-data": "1.13.5", 6 | "ember-cli-shims": "ember-cli/ember-cli-shims#0.0.3", 7 | "ember-cli-test-loader": "ember-cli-test-loader#0.1.3", 8 | "ember-load-initializers": "ember-cli/ember-load-initializers#0.1.4", 9 | "ember-qunit": "0.3.3", 10 | "ember-qunit-notifications": "0.0.7", 11 | "ember-resolver": "~0.1.15", 12 | "jquery": "^1.11.1", 13 | "loader.js": "ember-cli/loader.js#3.2.0", 14 | "qunit": "~1.17.1", 15 | "pretender": "~0.6.0", 16 | "ember-inflector": "~1.3.1", 17 | "lodash": "~3.7.0", 18 | "Faker": "3.0.0", 19 | "moment-timezone": "~0.4.0" 20 | }, 21 | "devDependencies": { 22 | "localforage": "1.2.4", 23 | "bootstrap": "~3.3.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | scenarios: [ 3 | { 4 | name: 'default', 5 | dependencies: { } 6 | } 7 | ] 8 | }; 9 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /* global require, module */ 2 | var EmberApp = require('ember-cli/lib/broccoli/ember-addon'); 3 | 4 | module.exports = function(defaults) { 5 | var app = new EmberApp(defaults, { 6 | // Add options here 7 | }); 8 | app.import(app.bowerDirectory + "/bootstrap/dist/css/bootstrap.min.css"); 9 | 10 | return app.toTree(); 11 | }; 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | module.exports = { 5 | name: 'ember-data-offline' 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-data-offline", 3 | "version": "0.2.0", 4 | "description": "Ember data extension to allow offline persistance and caching", 5 | "directories": { 6 | "doc": "doc", 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "start": "ember server", 11 | "build": "ember build", 12 | "test": "ember try:testall" 13 | }, 14 | "repository": "https://github.com/api-hogs/ember-data-offline.git", 15 | "engines": { 16 | "node": ">= 0.10.0" 17 | }, 18 | "author": "Alex Opak (https://github.com/OpakAlex)", 19 | "contributors": [ 20 | "Igor Kurinnoy (https://github.com/igorrKurr)" 21 | ], 22 | "license": "MIT", 23 | "devDependencies": { 24 | "broccoli-asset-rev": "^2.0.2", 25 | "ember-cli": "1.13.1", 26 | "ember-cli-app-version": "0.3.3", 27 | "ember-cli-content-security-policy": "0.4.0", 28 | "ember-cli-dependency-checker": "^1.0.0", 29 | "ember-cli-htmlbars": "0.7.9", 30 | "ember-cli-ic-ajax": "0.1.1", 31 | "ember-cli-inject-live-reload": "^1.3.0", 32 | "ember-cli-mirage": "0.1.5", 33 | "ember-cli-moment-shim": "0.3.3", 34 | "ember-cli-qunit": "0.3.13", 35 | "ember-cli-uglify": "^1.0.1", 36 | "ember-data": "1.13.4", 37 | "ember-disable-prototype-extensions": "^1.0.0", 38 | "ember-disable-proxy-controllers": "^1.0.0", 39 | "ember-export-application-global": "^1.0.2", 40 | "ember-localforage-adapter": "git+https://github.com/igorrKurr/ember-localforage-adapter.git", 41 | "ember-moment": "2.0.1", 42 | "ember-try": "0.0.7" 43 | }, 44 | "keywords": [ 45 | "ember-addon", 46 | "offline", 47 | "cache", 48 | "persist", 49 | "ember-data", 50 | "localforage", 51 | "queue", 52 | "sync", 53 | "synchronization" 54 | ], 55 | "dependencies": { 56 | "ember-cli-babel": "^5.0.0" 57 | }, 58 | "ember-addon": { 59 | "configPath": "tests/dummy/config" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "qunit", 3 | "test_page": "tests/index.html?hidepassed", 4 | "disable_watching": true, 5 | "parallel": 2, 6 | "phantomjs_debug_port": 9000, 7 | "launch_in_ci": [ 8 | "PhantomJS" 9 | ], 10 | "launch_in_dev": [ 11 | "PhantomJS", 12 | "Chrome" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "waitForRecordingAll", 4 | "waitForRecordingModel", 5 | "getLocalforageData", 6 | "stop", 7 | "start", 8 | "server", 9 | "document", 10 | "window", 11 | "location", 12 | "setTimeout", 13 | "$", 14 | "-Promise", 15 | "define", 16 | "console", 17 | "visit", 18 | "exists", 19 | "fillIn", 20 | "click", 21 | "keyEvent", 22 | "triggerEvent", 23 | "find", 24 | "findWithAssert", 25 | "wait", 26 | "DS", 27 | "andThen", 28 | "currentURL", 29 | "currentPath", 30 | "currentRouteName" 31 | ], 32 | "node": false, 33 | "browser": false, 34 | "boss": true, 35 | "curly": false, 36 | "debug": false, 37 | "devel": false, 38 | "eqeqeq": true, 39 | "evil": true, 40 | "forin": false, 41 | "immed": false, 42 | "laxbreak": false, 43 | "newcap": true, 44 | "noarg": true, 45 | "noempty": false, 46 | "nonew": false, 47 | "nomen": false, 48 | "onevar": false, 49 | "plusplus": false, 50 | "regexp": false, 51 | "undef": true, 52 | "sub": true, 53 | "strict": false, 54 | "white": false, 55 | "eqnull": true, 56 | "esnext": true 57 | } 58 | -------------------------------------------------------------------------------- /tests/acceptance/crud-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { module, test } from 'qunit'; 3 | import startApp from '../helpers/start-app'; 4 | import { assertRecordMeta, assertCollectionMeta } from '../helpers/assert-meta'; 5 | import { getLFObjectInfo } from '../helpers/lf-utils'; 6 | 7 | var App, users, cars, store; 8 | 9 | module('Acceptance: CRUD Test', { 10 | beforeEach: function() { 11 | Ember.run(() => { 12 | window.localforage.clear(); 13 | }); 14 | App = startApp(); 15 | users = server.createList('user', 2); 16 | cars = server.createList('car', 4); 17 | server.createList('city', 4); 18 | store = App.__container__.lookup('service:store'); 19 | }, 20 | afterEach: function() { 21 | Ember.run(App, 'destroy'); 22 | } 23 | }); 24 | 25 | test('findAll', function(assert) { 26 | assert.expect(7); 27 | 28 | visit('/'); 29 | 30 | waitForRecordingModel('user'); 31 | 32 | andThen(() => { 33 | return window.localforage.getItem('foo').then(result => { 34 | assert.equal(result.user.records[1].firstName, users[0].firstName, "Record 1 from server === record 1 in localforage"); 35 | assert.equal(result.user.records[2].firstName, users[1].firstName, "Record 2 from server === record 2 in localforage"); 36 | assert.equal(store.peekAll('user').get('firstObject').get('firstName'), users[0].firstName, "Record 1 in store === record 1 from server "); 37 | assert.equal(store.peekAll('user').get('lastObject').get('firstName'), users[1].firstName, "Record 2 in store === record 2 from server "); 38 | assert.equal(store.peekAll('user').get('firstObject').get('firstName'), result.user.records[1].firstName, "Record 1 in store === record 1 in localforage"); 39 | assert.equal(store.peekAll('user').get('lastObject').get('firstName'), result.user.records[2].firstName, "Record 2 in store === record 2 in localforage"); 40 | 41 | assertCollectionMeta(result.user, assert); 42 | }); 43 | }); 44 | }); 45 | 46 | test('find', function(assert) { 47 | assert.expect(5); 48 | 49 | visit('/users/1'); 50 | 51 | waitForRecordingModel('user'); 52 | 53 | andThen(() => { 54 | return window.localforage.getItem('foo').then(result => { 55 | let usersLF = getLFObjectInfo(result.user.records); 56 | 57 | assert.equal(usersLF.firstObject.firstName, users[0].firstName); 58 | assert.equal(usersLF.length, 1); 59 | }); 60 | }); 61 | 62 | waitForRecordingModel('car'); 63 | 64 | andThen(() => { 65 | return window.localforage.getItem('foo').then(result => { 66 | let carsLF = getLFObjectInfo(result.car.records); 67 | 68 | assert.equal(carsLF.firstObject.label, cars[0].label); 69 | assert.equal(carsLF.nth(1).label, cars[1].label); 70 | //as with findAll instead of findMany 71 | assert.equal(carsLF.length, 4); 72 | }); 73 | }); 74 | }); 75 | 76 | test('createRecord', function(assert) { 77 | assert.expect(7); 78 | 79 | visit('/'); 80 | 81 | andThen(() => { 82 | return click('#add-user'); 83 | }); 84 | 85 | waitForRecordingModel('user', 4); 86 | 87 | andThen(() => { 88 | return window.localforage.getItem('foo').then(result => { 89 | let users = getLFObjectInfo(result.user.records); 90 | let newUser = users.find("firstName", "Igor"); 91 | 92 | assert.equal(users.length, 3, "There is new record in localforage"); 93 | assert.equal(newUser.firstName, "Igor", "Created record from server === created record in localforage"); 94 | assert.ok(newUser.id.length > 3); 95 | assert.equal(newUser.firstName, store.peekRecord('user', newUser.id).get('firstName'), "Created record from store === created record in localforage"); 96 | 97 | assertCollectionMeta(result.user, assert); 98 | assertRecordMeta(newUser, assert); 99 | }); 100 | }); 101 | }); 102 | 103 | test('deleteRecord', function(assert) { 104 | assert.expect(3); 105 | 106 | visit('/'); 107 | 108 | waitForRecordingModel('user'); 109 | 110 | andThen(() => { 111 | return click('.delete-user:first'); 112 | }); 113 | 114 | waitForRecordingModel('user', 2); 115 | 116 | andThen(() => { 117 | return window.localforage.getItem('foo').then(result => { 118 | let users = getLFObjectInfo(result.user.records); 119 | 120 | assert.equal(users.length, 1, "Record was deleted from LF"); 121 | assert.equal(store.peekAll('user').get('length'), 1, "Record was deleted from store"); 122 | assertCollectionMeta(result.user, assert); 123 | }); 124 | }); 125 | }); 126 | 127 | test('updateRecord', function(assert) { 128 | assert.expect(5); 129 | 130 | visit('/users/1'); 131 | 132 | waitForRecordingModel('user'); 133 | 134 | andThen(() => { 135 | fillIn('#usr-first-name', "New name"); 136 | click('#update-user'); 137 | }); 138 | 139 | waitForRecordingModel('user'); 140 | 141 | andThen(() => { 142 | return window.localforage.getItem('foo').then(result => { 143 | let users = getLFObjectInfo(result.user.records); 144 | 145 | assert.equal(users.length, 1, "No additional records were added"); 146 | assert.equal(store.peekRecord('user', 1).get('firstName'), "New name", "Record prop was updated in store"); 147 | assert.equal(users.firstObject.firstName, "New name", "Record prop was updated in LF"); 148 | 149 | assertRecordMeta(users.firstObject, assert); 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /tests/acceptance/stress-test.js: -------------------------------------------------------------------------------- 1 | /*TODO move this to special category of tests like load test 2 | * or do not run on trevis somehow 3 | * for now run manually*/ 4 | 5 | // import Ember from 'ember'; 6 | // import { module, test } from 'qunit'; 7 | // import startApp from '../helpers/start-app'; 8 | // import { assertRecordMeta, assertCollectionMeta } from '../helpers/assert-meta'; 9 | // import { getLFObjectInfo } from '../helpers/lf-utils'; 10 | 11 | // var App, store; 12 | 13 | // module('Acceptance: Stress Test', { 14 | // beforeEach: function() { 15 | // Ember.run(() => { 16 | // window.localforage.clear(); 17 | // }); 18 | // App = startApp(); 19 | 20 | // server.createList('user', 1000); 21 | // server.createList('car', 1000); 22 | // server.createList('company', 1000); 23 | // server.createList('office', 1000); 24 | // server.createList('city', 1000); 25 | 26 | // store = App.__container__.lookup('service:store'); 27 | // }, 28 | // afterEach: function() { 29 | // Ember.run(App, 'destroy'); 30 | // } 31 | // }); 32 | 33 | // test('findAll', function(assert) { 34 | // assert.expect(10); 35 | 36 | // visit('/stress'); 37 | 38 | // waitForRecordingModel('car', 3); 39 | 40 | // andThen(() => { 41 | // return window.localforage.getItem('foo').then(result => { 42 | // assert.equal(getLFObjectInfo(result.car.records).length, 1000); 43 | // assertCollectionMeta(result.car, assert); 44 | 45 | // assert.equal(getLFObjectInfo(result.city.records).length, 1000); 46 | // assertCollectionMeta(result.city, assert); 47 | 48 | // assert.equal(getLFObjectInfo(result.company.records).length, 1000); 49 | // assertCollectionMeta(result.company, assert); 50 | // }); 51 | // }); 52 | 53 | // waitForRecordingModel('user'); 54 | 55 | // andThen(() => { 56 | // return window.localforage.getItem('foo').then(result => { 57 | // assert.equal(getLFObjectInfo(result.user.records).length, 1000); 58 | // assertCollectionMeta(result.user, assert); 59 | // }); 60 | // }); 61 | 62 | // waitForRecordingModel('office'); 63 | 64 | // andThen(() => { 65 | // return window.localforage.getItem('foo').then(result => { 66 | // assert.equal(getLFObjectInfo(result.office.records).length, 1000); 67 | // assertCollectionMeta(result.office, assert); 68 | // }); 69 | // }); 70 | // }); 71 | -------------------------------------------------------------------------------- /tests/dummy/app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import baseAdapter from 'ember-data-offline/adapters/base'; 2 | import config from '../config/environment'; 3 | 4 | export default baseAdapter.extend({ 5 | offlineNamespace: config.ns, 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/adapters/car.js: -------------------------------------------------------------------------------- 1 | import appAdapter from './application'; 2 | import moment from 'moment'; 3 | 4 | export default appAdapter.extend({ 5 | recordTTL: moment.duration(1, 'minute'), 6 | throttle: 100 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/adapters/company.js: -------------------------------------------------------------------------------- 1 | import appAdapter from './application'; 2 | import moment from 'moment'; 3 | 4 | export default appAdapter.extend({ 5 | collectionTTL: moment.duration(1, 'hour'), 6 | recordTTL: moment.duration(5, 'minutes'), 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/adapters/office.js: -------------------------------------------------------------------------------- 1 | import appAdapter from './application'; 2 | 3 | export default appAdapter.extend({ 4 | pathForType() { 5 | return "offices_for_company"; 6 | } 7 | }); 8 | 9 | -------------------------------------------------------------------------------- /tests/dummy/app/adapters/user.js: -------------------------------------------------------------------------------- 1 | import appAdapter from './application'; 2 | 3 | export default appAdapter.extend({ 4 | serializerPrimaryKey: '_id', 5 | // skipCreateReplacing: true, 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from 'ember/resolver'; 3 | import loadInitializers from 'ember/load-initializers'; 4 | import config from './config/environment'; 5 | 6 | var App; 7 | 8 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver: Resolver 14 | }); 15 | 16 | //Shut up deprecations 17 | Ember.deprecate = function() { }; 18 | Ember.warn = function() { }; 19 | 20 | loadInitializers(App, config.modulePrefix); 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/api-hogs/ember-data-offline/920597742e88d6961ce7cf34332d308a458dc533/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/api-hogs/ember-data-offline/920597742e88d6961ce7cf34332d308a458dc533/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/api-hogs/ember-data-offline/920597742e88d6961ce7cf34332d308a458dc533/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for 'head'}} 11 | 12 | 13 | 14 | 15 | {{content-for 'head-footer'}} 16 | 17 | 18 | {{content-for 'body'}} 19 | 20 | 21 | 22 | 23 | {{content-for 'body-footer'}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/mirage/config.js: -------------------------------------------------------------------------------- 1 | /*global Uint8Array*/ 2 | import Mirage from 'ember-cli-mirage'; 3 | 4 | var genId = function() { 5 | var arr = new Uint8Array(8); 6 | window.crypto.getRandomValues(arr); 7 | return [].map.call(arr, function(n) { return n.toString(16); }).join(""); 8 | }; 9 | 10 | export default function() { 11 | this.get('/users', function(db, req){ 12 | return {dummy_users: db.users}; 13 | }); 14 | this.get('/users/:id', function(db, req){ 15 | let user = db.users.find(req.params.id); 16 | return {dummy_user: user}; 17 | }); 18 | this.post('/users', function(db, request) { 19 | var attrs = JSON.parse(request.requestBody)['user']; 20 | attrs._id = genId(); 21 | delete attrs.id; 22 | // return new Mirage.Response(404, null, null); // Need this for testing 23 | return {dummy_user: attrs}; 24 | }); 25 | this.put('/users/:id', function(db, request) { 26 | var attrs = JSON.parse(request.requestBody)['user']; 27 | // return new Mirage.Response(408, null, null); // Need this for testing 28 | return {dummy_user: attrs}; 29 | }); 30 | this.put('/update_users', function(db, request) { 31 | // var attrs = JSON.parse(request.requestBody)['user']; 32 | return new Mirage.Response(408, null, null); // Need this for testing 33 | // return {dummy_user: attrs}; 34 | }); 35 | this.del('/users/:id', 'user'); 36 | 37 | this.get('/companies', function(db, req){ 38 | 39 | if (req.queryParams.firstTwo) { 40 | return {companies: db.companies.slice(0,2)}; 41 | } 42 | return {companies: db.companies}; 43 | }); 44 | 45 | this.get('/cars', function(db, req){ 46 | return {cars: db.cars}; 47 | }); 48 | 49 | this.get('/offices_for_company', function(db, req){ 50 | return {offices: db.offices}; 51 | }); 52 | this.get('/offices_for_company/:id', function(db, req){ 53 | let office = db.offices.find(req.params.id); 54 | return {offices: office}; 55 | }); 56 | 57 | this.get('/cities', function(db, req){ 58 | return {cities: db.cities}; 59 | }); 60 | this.get('/cities/:id', function(db, req){ 61 | let city = db.cities.find(req.params.id); 62 | return {city: city}; 63 | }); 64 | 65 | this.pretender.get('/*passthrough', this.pretender.passthrough); 66 | } 67 | -------------------------------------------------------------------------------- /tests/dummy/app/mirage/factories/car.js: -------------------------------------------------------------------------------- 1 | import Mirage, {faker} from 'ember-cli-mirage'; 2 | 3 | export default Mirage.Factory.extend({ 4 | label: function(i) { 5 | return "Car # " + (i + 1); 6 | }, 7 | user: function(i) { 8 | let id = i + 1; 9 | return id % 2 === 0 ? id / 2 : (i === 0) ? 1 : i; 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /tests/dummy/app/mirage/factories/city.js: -------------------------------------------------------------------------------- 1 | import Mirage, { faker } from 'ember-cli-mirage'; 2 | 3 | export default Mirage.Factory.extend({ 4 | name: faker.address.city, 5 | office: function(i) { 6 | let office = { 7 | id: i + 1, 8 | address: faker.address.country(), 9 | city: i + 1, 10 | company: i + 1 11 | }; 12 | return office; 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /tests/dummy/app/mirage/factories/company.js: -------------------------------------------------------------------------------- 1 | import Mirage, {faker} from 'ember-cli-mirage'; 2 | 3 | export default Mirage.Factory.extend({ 4 | name: faker.company.companyName, 5 | office: function(i) { 6 | let id = i + 1; 7 | return id; 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /tests/dummy/app/mirage/factories/office.js: -------------------------------------------------------------------------------- 1 | import Mirage, {faker} from 'ember-cli-mirage'; 2 | 3 | export default Mirage.Factory.extend({ 4 | address: faker.address.streetAddress, 5 | company: function(i) { 6 | let id = i + 1; 7 | return id; 8 | }, 9 | city: function(i) { 10 | let id = i + 1; 11 | return id; 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /tests/dummy/app/mirage/factories/user.js: -------------------------------------------------------------------------------- 1 | import Mirage, {faker} from 'ember-cli-mirage'; 2 | 3 | export default Mirage.Factory.extend({ 4 | _id: function(i) { 5 | return i + 1; 6 | }, 7 | firstName: faker.name.firstName, 8 | lastName: faker.name.lastName, 9 | gender: faker.list.cycle('male', 'female'), 10 | cars: function(i) { 11 | let id = i + 1; 12 | return [2 * id - 1, 2 * id]; 13 | }, 14 | skills: function(i) { 15 | let id = i + 1; 16 | return [{id: id, title: `Skill#${i}`}, {id: id + 1, title: `Skill#${i + 1}`}]; 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /tests/dummy/app/mirage/scenarios/default.js: -------------------------------------------------------------------------------- 1 | export default function(server) { 2 | server.createList('user', 50); 3 | server.createList('car', 100); 4 | server.createList('company', 3); 5 | server.createList('office', 100); 6 | server.createList('city', 100); 7 | 8 | //TODO: find way to do separate stress testing 9 | //for stress testing 10 | // server.createList('user', 500); 11 | // server.createList('car', 1000); 12 | // server.createList('company', 300); 13 | // server.createList('office', 300); 14 | // server.createList('city', 500); 15 | } 16 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/api-hogs/ember-data-offline/920597742e88d6961ce7cf34332d308a458dc533/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/models/car.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | const { Model, attr, belongsTo } = DS; 3 | 4 | export default Model.extend({ 5 | label: attr('string'), 6 | user: belongsTo('user', {async: true}) 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/models/city.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | const { Model, attr, belongsTo } = DS; 3 | 4 | export default Model.extend({ 5 | name: attr('string'), 6 | office: belongsTo('office') 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/models/company.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | const { Model, attr, belongsTo } = DS; 3 | 4 | export default Model.extend({ 5 | name: attr('string'), 6 | office: belongsTo('office', {async: true}) 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/models/office.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | const { Model, attr, belongsTo } = DS; 3 | 4 | export default Model.extend({ 5 | address: attr('string'), 6 | company: belongsTo('company', {async: true}), 7 | city: belongsTo('city', {async: true}) 8 | }); 9 | -------------------------------------------------------------------------------- /tests/dummy/app/models/user.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | const { Model, attr, hasMany } = DS; 3 | 4 | export default Model.extend({ 5 | firstName: attr('string'), 6 | lastName: attr('string'), 7 | gender: attr('string'), 8 | cars: hasMany('car', {async: true}), 9 | skills: attr('json') 10 | }); 11 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/cities/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model() { 5 | return this.store.findAll('city'); 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/cities/template.hbs: -------------------------------------------------------------------------------- 1 |

Cities

2 | 7 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/companies/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model() { 5 | return this.store.findAll('company'); 6 | }, 7 | actions: { 8 | reload() { 9 | this.store.forceFetchAll('company'); 10 | } 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/companies/template.hbs: -------------------------------------------------------------------------------- 1 |

Companies

2 | 3 | 8 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/offices/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model() { 5 | return this.store.findAll('office'); 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/offices/template.hbs: -------------------------------------------------------------------------------- 1 |

Offices

2 | 7 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/stress/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model(){ 5 | return Ember.RSVP.hash({ 6 | city: this.store.findAll('city'), 7 | user: this.store.findAll('user'), 8 | car: this.store.findAll('car'), 9 | company: this.store.findAll('company'), 10 | office: this.store.findAll('office'), 11 | }); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/stress/template.hbs: -------------------------------------------------------------------------------- 1 |

2 | Pretty much models here 3 |

4 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/users/index/controller.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | isUserSyncLoad: Ember.computed.equal('store.syncLoads.findAll.user', true), 5 | }); 6 | 7 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/users/index/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model() { 5 | this.store.findAll('city'); 6 | return this.store.findAll('user'); 7 | }, 8 | 9 | actions: { 10 | createUser() { 11 | let newUser = this.store.createRecord('user', { 12 | firstName: "Igor", 13 | lastName: "K", 14 | gender: "male" 15 | }); 16 | newUser.save().then(result => { 17 | console.log("saved!", result); 18 | }); 19 | }, 20 | updateFirst() { 21 | let url = '/update_users'; 22 | let data = { 23 | firstName: 'Aaron' 24 | }; 25 | this.EDORequest.exec(url, 'PUT', data, () => { 26 | this.set('currentModel.firstObject.firstName', 'Aaron'); 27 | this.get('currentModel.firstObject').save(); 28 | }); 29 | }, 30 | deleteUser(user) { 31 | user.destroyRecord(); 32 | }, 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/users/index/template.hbs: -------------------------------------------------------------------------------- 1 |

2 | Users 3 | 4 | 5 |

6 |

Is sync loaded: {{isUserSyncLoad}}

7 | 17 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/users/template.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/users/user/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model(params){ 5 | return this.store.findRecord('user', params.id); 6 | }, 7 | actions: { 8 | updateUser() { 9 | this.get('currentModel').save(); 10 | }, 11 | reload(id) { 12 | this.store.forceFetchRecord('user', id); 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/users/user/template.hbs: -------------------------------------------------------------------------------- 1 | {{#link-to 'users'}}Back{{/link-to}} 2 |

3 | Page for user: {{model.id}} 4 |

5 | 6 |
7 |
8 |
9 |
10 | 11 | {{input id='usr-first-name' value=model.firstName class="form-control"}} 12 |
13 |
14 | 15 | {{input value=model.lastName class="form-control"}} 16 |
17 | 18 |
19 |
20 |
21 |
22 |
23 |

And his cars:

24 | 25 | {{#each model.cars as |car|}} 26 |
  • {{car.id}} - {{car.label}}
  • 27 | {{/each}} 28 |
    29 |
    30 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | 4 | var Router = Ember.Router.extend({ 5 | location: config.locationType 6 | }); 7 | 8 | Router.map(function() { 9 | this.route('users', {path: '/'}, function() { 10 | this.route('index', {path: '/'}); 11 | this.route('user', {path: '/users/:id'}); 12 | }); 13 | this.route('companies'); 14 | this.route('offices'); 15 | this.route('cities'); 16 | this.route('stress'); 17 | }); 18 | 19 | export default Router; 20 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/api-hogs/ember-data-offline/920597742e88d6961ce7cf34332d308a458dc533/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/routes/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | actions: { 5 | goOnline() { 6 | window.navigator.__defineGetter__('onLine', function() { 7 | return true; 8 | }); 9 | $(window).trigger('online'); 10 | this.set('controller.isOffline', false); 11 | }, 12 | goOffline() { 13 | window.navigator.__defineGetter__('onLine', function() { 14 | return false; 15 | }); 16 | $(window).trigger('offline'); 17 | this.set('controller.isOffline', true); 18 | }, 19 | toggleOffline() { 20 | if (this.get('controller.isOffline')) { 21 | this.send('goOnline'); 22 | } 23 | else { 24 | this.send('goOffline'); 25 | } 26 | } 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /tests/dummy/app/serializers/application.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.RESTSerializer.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /tests/dummy/app/serializers/city.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, { 4 | attrs: { 5 | office: { embedded: 'always' }, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/serializers/user.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.RESTSerializer.extend({ 4 | primaryKey: '_id', 5 | modelNameFromPayloadKey: function(payloadKey) { 6 | if (payloadKey === 'dummy_users' || payloadKey === 'dummy_user') { 7 | return this._super('user'); 8 | } else { 9 | return this._super(payloadKey); 10 | } 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/api-hogs/ember-data-offline/920597742e88d6961ce7cf34332d308a458dc533/tests/dummy/app/styles/app.css -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | 35 |
    36 | {{outlet}} 37 |
    38 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/api-hogs/ember-data-offline/920597742e88d6961ce7cf34332d308a458dc533/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/transforms/json.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Transform.extend({ 4 | deserialize(serialized) { 5 | return serialized; 6 | }, 7 | 8 | serialize(deserialized) { 9 | //TODO Mention this in README. This is quite important thing, that can break down persistance 10 | if (deserialized) { 11 | return JSON.parse(JSON.stringify(deserialized)); 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | var ENV = { 5 | modulePrefix: 'dummy', 6 | podModulePrefix: 'dummy/pods', 7 | environment: environment, 8 | baseURL: '/', 9 | locationType: 'auto', 10 | EmberENV: { 11 | FEATURES: { 12 | // Here you can enable experimental features on an ember canary build 13 | // e.g. 'with-controller': true 14 | } 15 | }, 16 | 17 | APP: { 18 | // Here you can pass flags/options to your application instance 19 | // when it is created 20 | } 21 | }; 22 | 23 | // ENV['ember-data-offline'] = { 24 | // enabled: false 25 | // }; 26 | 27 | if (environment === 'development') { 28 | // ENV['ember-cli-mirage'] = { enabled: false }; 29 | 30 | // ENV.APP.LOG_RESOLVER = true; 31 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 32 | // ENV.APP.LOG_TRANSITIONS = true; 33 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 34 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 35 | ENV.ns = 'ember-data-offline:dummy'; 36 | } 37 | 38 | if (environment === 'test') { 39 | // Testem prefers this... 40 | ENV.baseURL = '/'; 41 | ENV.locationType = 'none'; 42 | 43 | // keep test console output quieter 44 | ENV.APP.LOG_ACTIVE_GENERATION = false; 45 | ENV.APP.LOG_VIEW_LOOKUPS = false; 46 | 47 | ENV.APP.rootElement = '#ember-testing'; 48 | 49 | ENV.ns = 'foo'; 50 | } 51 | 52 | if (environment === 'production') { 53 | 54 | } 55 | 56 | return ENV; 57 | }; 58 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/assert-meta.js: -------------------------------------------------------------------------------- 1 | var _assertMeta = function(isAll, obj, assert) { 2 | let fetchedAt = obj['__data_offline_meta__'].fetchedAt; 3 | assert.ok(fetchedAt, "Record meta present"); 4 | if (!isAll) { 5 | let updatedAt = obj['__data_offline_meta__'].updatedAt; 6 | assert.ok(updatedAt, "Record meta present"); 7 | } 8 | }; 9 | 10 | var assertRecordMeta = function assertRecordMeta(obj, assert) { 11 | _assertMeta(false, obj, assert); 12 | }; 13 | 14 | var assertCollectionMeta = function assertCollectionMeta(obj, assert) { 15 | _assertMeta(true, obj, assert); 16 | }; 17 | 18 | export { assertRecordMeta, assertCollectionMeta }; 19 | -------------------------------------------------------------------------------- /tests/helpers/base.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import moment from 'moment'; 3 | 4 | const { RSVP } = Ember; 5 | 6 | (function(global) { 7 | let fakeNavigator = {}; 8 | for (let i in global.navigator) { 9 | fakeNavigator[i] = global.navigator[i]; 10 | } 11 | global.navigator = fakeNavigator; 12 | }(window)); 13 | 14 | var goOffline = function goOffline() { 15 | return new RSVP.Promise(resolve => { 16 | window.navigator.__defineGetter__('onLine', function() { 17 | return false; 18 | }); 19 | $(window).trigger('offline'); 20 | resolve(); 21 | }); 22 | }; 23 | 24 | var goOnline = function goOnline() { 25 | window.navigator.__defineGetter__('onLine', function() { 26 | return true; 27 | }); 28 | $(window).trigger('online'); 29 | }; 30 | 31 | var getStoreMock = function() { 32 | return Ember.Object.create({ 33 | peekAll(){ 34 | return Ember.A([ 35 | getSnapshotMock() 36 | ]); 37 | }, 38 | peekRecord(modelName, id){ 39 | if(id === 'foo'){ 40 | return getSnapshotMock(); 41 | } 42 | }, 43 | serializerFor(){ 44 | return { 45 | primaryKey: 'id', 46 | normalizePayload(payload) { 47 | return payload; 48 | }, 49 | modelNameFromPayloadKey(key) { 50 | return key; 51 | } 52 | }; 53 | }, 54 | metadataFor(){ 55 | return Ember.Object.create(); 56 | }, 57 | lookupAdapter() { 58 | return Ember.Object.create({ 59 | collectionTTL : moment.duration(12, 'hours'), 60 | recordTTL : moment.duration(12, 'hours') 61 | }); 62 | } 63 | }); 64 | }; 65 | 66 | var getResultMock = function() { 67 | return { 68 | name: 'foo' 69 | }; 70 | }; 71 | 72 | var getResultFromPayloadMock = function() { 73 | return { 74 | name: 'foo2' 75 | }; 76 | }; 77 | 78 | var getTypeMock = function(){ 79 | return Ember.Object.create({ 80 | modelName : 'bar' 81 | }); 82 | }; 83 | 84 | var getSnapshotMock = function() { 85 | return Ember.Object.create({ 86 | record : Ember.Object.create({ 87 | store : getStoreMock(), 88 | __data_offline_meta__ : Ember.Object.create() 89 | }), 90 | id : 'foo', 91 | _internalModel : getTypeMock() 92 | }); 93 | }; 94 | 95 | var getQueueMock = function(assert, encapsulatedIn) { 96 | return Ember.Object.create({ 97 | _assert: assert, 98 | _encapsulatedIn: encapsulatedIn, 99 | add() { 100 | this.get('_assert').ok(true, `queue.add was invoked @ ${this.get('_encapsulatedIn')}`); 101 | } 102 | }); 103 | }; 104 | 105 | var getMetaMock = function(){ 106 | return { 107 | __data_offline_meta__ : { 108 | fetchedAt : moment() 109 | } 110 | }; 111 | }; 112 | 113 | //setup assert 114 | var getAdapterMock = function(adapterName){ 115 | return Ember.Object.create({ 116 | __adapterName__: adapterName, 117 | findAll(){ 118 | this.get('assert').ok(true, `findAll was invoked @ ${this.get('__adapterName__')} adapter`); 119 | return Ember.RSVP.Promise.resolve(Ember.A([getResultMock()])); 120 | }, 121 | find(){ 122 | this.get('assert').ok(true, `find was invoked @ ${this.get('__adapterName__')} adapter`); 123 | //simulate if param id equals 2 then return empty record 124 | return (arguments[2] === 'no_record') ? Ember.RSVP.Promise.resolve() : Ember.RSVP.Promise.resolve(getResultMock()); 125 | }, 126 | query(){ 127 | this.get('assert').ok(true, `query was invoked @ ${this.get('__adapterName__')} adapter`); 128 | //simulate if param id equals 2 then return empty record 129 | return (arguments[2] === 'no_record') ? Ember.RSVP.Promise.resolve(Ember.A()) : Ember.RSVP.Promise.resolve(Ember.A([getResultMock()])); 130 | }, 131 | findQuery(){ 132 | this.get('assert').ok(true, `findQuery was invoked @ ${this.get('__adapterName__')} adapter`); 133 | //simulate if param id equals 2 then return empty record 134 | return Ember.RSVP.Promise.resolve(Ember.A([getResultMock()])); 135 | }, 136 | findMany(){ 137 | this.get('assert').ok(true, `findMany was invoked @ ${this.get('__adapterName__')} adapter`); 138 | //simulate if param id equals 2 then return empty record 139 | return (arguments[2][0] === 'no_record') ? Ember.RSVP.Promise.resolve(Ember.A()) : Ember.RSVP.Promise.resolve(Ember.A([getResultMock()])); 140 | }, 141 | createRecord(){ 142 | this.get('assert').ok(true, `createRecord was invoked @ ${this.get('__adapterName__')} adapter`); 143 | return Ember.RSVP.Promise.resolve(getSnapshotMock()); 144 | }, 145 | updateRecord(){ 146 | this.get('assert').ok(true, `updateRecord was invoked @ ${this.get('__adapterName__')} adapter`); 147 | return Ember.RSVP.Promise.resolve(getSnapshotMock()); 148 | }, 149 | deleteRecord(){ 150 | this.get('assert').ok(true, `deleteRecord was invoked @ ${this.get('__adapterName__')} adapter`); 151 | return Ember.RSVP.Promise.resolve(getSnapshotMock()); 152 | }, 153 | _namespaceForType(){ 154 | this.get('assert').ok(true, `_namespaceForType was invoked @ ${this.get('__adapterName__')} adapter`); 155 | let metaMock = getMetaMock(); 156 | metaMock.fetchedAt = moment().subtract(13, 'hours').calendar(); //outdated because ttl = 12 hours 157 | return Ember.RSVP.Promise.resolve(metaMock); 158 | } 159 | }); 160 | }; 161 | 162 | export { 163 | goOnline, goOffline, getAdapterMock, getStoreMock, getQueueMock, getSnapshotMock, getMetaMock, 164 | getTypeMock, getResultMock, getResultFromPayloadMock 165 | }; 166 | -------------------------------------------------------------------------------- /tests/helpers/job.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import LocalstorageJob from 'ember-data-offline/jobs/localforage'; 3 | import RESTJob from 'ember-data-offline/jobs/rest'; 4 | 5 | const { RSVP } = Ember; 6 | 7 | var resolveMock = function(dataMock){ 8 | return dataMock; 9 | }; 10 | var emberModelMock = Ember.Object.extend({ 11 | _createSnapshot(){ 12 | return snapshotMock.reopen({ id : 'foo'}); 13 | } 14 | }); 15 | var storeMock = Ember.Object.extend({ 16 | peekAll(){ 17 | return Ember.A([ 18 | emberModelMock.create({id: 'foo'}) 19 | ]); 20 | }, 21 | peekRecord(modelName, id){ 22 | if(id === 'foo'){ 23 | return emberModelMock.create({id: 'foo'}); 24 | } 25 | }, 26 | serializerFor(){ 27 | return { 28 | primaryKey: 'id', 29 | normalizePayload(payload) { 30 | return payload; 31 | }, 32 | modelNameFromPayloadKey(key) { 33 | return key; 34 | } 35 | }; 36 | }, 37 | metadataFor(){ 38 | return Ember.Object.create(); 39 | } 40 | }); 41 | 42 | var snapshotMock = Ember.Object.create({ 43 | record : Ember.Object.create({ 44 | store : storeMock.create(), 45 | __data_offline_meta__ : Ember.Object.create() 46 | }), 47 | _internalModel : { 48 | modelName : 'bar' 49 | } 50 | }); 51 | 52 | var adapterСlass = Ember.Object.extend({ 53 | createRecord() { 54 | this.get('assert').ok(true, this.get('adapterType') + " adapter.createRecord was invoked."); 55 | return RSVP.Promise.resolve({bar : {id : 'foo'}}); 56 | }, 57 | updateRecord() { 58 | this.get('assert').ok(true, this.get('adapterType') + " adapter.updateRecord was invoked."); 59 | return RSVP.Promise.resolve({bar : {id : 'foo'}}); 60 | }, 61 | deleteRecord(){ 62 | this.get('assert').ok(true, this.get('adapterType') + " adapter.deleteRecoed was invoked."); 63 | return RSVP.Promise.resolve({bar : {id : 'foo'}}); 64 | }, 65 | unhandled(){ 66 | this.get('assert').ok(true, this.get('adapterType') + " adapter.unhandled was invoked."); 67 | return RSVP.Promise.resolve({bar : {id : 'foo'}}); 68 | }, 69 | queue : { 70 | attach: callback => { 71 | callback(function(){ }); 72 | } 73 | }, 74 | _namespaceForType(typeClass){ 75 | this.get('assert').ok(true, this.get('adapterType') + " adapter._namespaceForType was invoked."); 76 | return RSVP.Promise.resolve({ records : Ember.A(), __data_offline_meta__ : Ember.Object.create()}); 77 | }, 78 | persistData(){ 79 | this.get('assert').ok(true, this.get('adapterType') + " adapter.persistData was invoked."); 80 | return RSVP.Promise.resolve(); 81 | }, 82 | serializer : Ember.Object.create({ 83 | serialize(snapshot){ 84 | return { 85 | id : snapshot.id, 86 | __data_offline_meta__ : snapshot.record.get('__data_offline_meta__') 87 | }; 88 | } 89 | }) 90 | }); 91 | 92 | var typeClassMock = { 93 | modelName: 'bar', 94 | }; 95 | 96 | 97 | var localstorageJobMock = function(assert, onlineAdapterResp, method = { name : 'find', args : 1}) { 98 | let offlineAdapter = adapterСlass.create({ 99 | assert : assert, 100 | adapterType : "offline" 101 | }); 102 | 103 | let _storeMock = storeMock.create({}); 104 | let job = LocalstorageJob.create({ 105 | adapter: offlineAdapter, 106 | }); 107 | job.set('method', method.name); 108 | 109 | job.set('params', [_storeMock, typeClassMock, method.args, snapshotMock]); 110 | 111 | if (method.name === 'findAll') { 112 | job.set('params', [_storeMock, typeClassMock, 'sinceToken']); 113 | return job; 114 | } 115 | 116 | if (method.name === 'findQuery') { 117 | job.set('params', [_storeMock, typeClassMock, method.args]); 118 | return job; 119 | } 120 | 121 | if (method.name === 'findMany') { 122 | job.set('params', [_storeMock, typeClassMock, method.args, 'sinceToken']); 123 | return job; 124 | } 125 | 126 | if (method.name === 'deleteRecord' || method.name === 'updateRecord'){ 127 | job.set('params', [_storeMock, typeClassMock, snapshotMock, onlineAdapterResp]); 128 | return job; 129 | } 130 | 131 | return job; 132 | }; 133 | 134 | var restJobMock = function function_name(assert, method = { name : 'find', args : 1}) { 135 | 136 | let onlineAdapter = adapterСlass.create({ 137 | assert : assert, 138 | adapterType : "rest", 139 | findAll(){ 140 | this.get('assert').ok(true, this.get('adapterType') + " adapter.findAll was invoked."); 141 | return Ember.RSVP.Promise.resolve({bar : {id : 'foo'}}); 142 | }, 143 | find(){ 144 | this.get('assert').ok(true, this.get('adapterType') + " adapter.find was invoked."); 145 | return Ember.RSVP.Promise.resolve({bar : {id : 'foo'}}); 146 | }, 147 | findQuery(){ 148 | this.get('assert').ok(true, this.get('adapterType') + " adapter.findQuery was invoked."); 149 | return Ember.RSVP.Promise.resolve({bar : {id : 'foo'}}); 150 | }, 151 | findMany(){ 152 | this.get('assert').ok(true, this.get('adapterType') + " adapter.findMany was invoked."); 153 | return Ember.RSVP.Promise.resolve({bar : {id : 'foo'}}); 154 | }, 155 | offlineAdapter : adapterСlass.create({ 156 | assert : assert, 157 | adapterType : "offline" 158 | }), 159 | createOfflineJob(){ 160 | this.get('assert').ok(true, this.get('adapterType') + " adapter.createOfflineJob was invoked."); 161 | return Ember.RSVP.Promise.resolve('foo'); 162 | } 163 | }); 164 | 165 | let _storeMosck = storeMock.create({ 166 | syncLoads : { 167 | find : {}, 168 | findAll: {}, 169 | findMany: {}, 170 | findQuery: {} 171 | }, 172 | pushPayload(){ 173 | assert.ok(true, "store.pushPayload was invoked"); 174 | }, 175 | unloadRecord(){ 176 | assert.ok(true, "store.unloadRecord was invoked"); 177 | }, 178 | deleteRecord(){ 179 | assert.ok(true, "store.deleteRecord was invoked"); 180 | } 181 | }); 182 | 183 | let job = RESTJob.create({ 184 | adapter: onlineAdapter, 185 | }); 186 | 187 | job.set('method', method.name); 188 | 189 | job.set('params', [_storeMosck, typeClassMock, method.args, snapshotMock]); 190 | 191 | if (method.name === 'findAll') { 192 | job.set('params', [_storeMosck, typeClassMock, 'sinceToken']); 193 | return job; 194 | } 195 | 196 | if (method.name === 'findQuery') { 197 | job.set('params', [_storeMosck, typeClassMock, method.args]); 198 | return job; 199 | } 200 | 201 | if (method.name === 'findMany') { 202 | job.set('params', [_storeMosck, typeClassMock, method.args, 'sinceToken']); 203 | return job; 204 | } 205 | 206 | if(method.name ==='createRecord' || method.name ==='updateRecord' || method.name ==='deleteRecord' ){ 207 | job.set('params', [_storeMosck, typeClassMock, snapshotMock, {}]); 208 | return job; 209 | } 210 | 211 | return job; 212 | }; 213 | export { 214 | localstorageJobMock, 215 | restJobMock 216 | }; 217 | -------------------------------------------------------------------------------- /tests/helpers/lf-utils.js: -------------------------------------------------------------------------------- 1 | var getLFObjectInfo = function getLFObjectInfo(obj) { 2 | let keys = Object.keys(obj); 3 | let length = keys.length; 4 | let firstObject = obj[keys[0]]; 5 | let lastObject = obj[keys[length - 1]]; 6 | 7 | return { 8 | length: length, 9 | firstObject: firstObject, 10 | lastObject: lastObject, 11 | nth(n) { 12 | return obj[keys[n]]; 13 | }, 14 | find(prop, val) { 15 | let key = keys.filter(key => { 16 | return obj[key][prop] === val; 17 | })[0]; 18 | return obj[key]; 19 | } 20 | }; 21 | }; 22 | 23 | export { getLFObjectInfo }; 24 | -------------------------------------------------------------------------------- /tests/helpers/localforage-helpers.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | var localforageHelpers = function() { 4 | 5 | Ember.Test.registerAsyncHelper('waitForRecordingModel', function(app, model, numberOfRecords = 1) { 6 | return Ember.Test.promise(function(resolve) { 7 | Ember.Test.adapter.asyncStart(); 8 | 9 | let interval = window.setInterval(function() { 10 | let adapter = app.__container__.lookup('service:store').adapterFor(model); 11 | let counter = adapter.get('queue.counter'); 12 | if (counter >= numberOfRecords) { 13 | window.clearInterval(interval); 14 | Ember.Test.adapter.asyncEnd(); 15 | Ember.run(null, resolve, true); 16 | } 17 | }, 100); 18 | }); 19 | }); 20 | 21 | }(); 22 | 23 | export default localforageHelpers; 24 | -------------------------------------------------------------------------------- /tests/helpers/offline.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { RSVP } = Ember; 3 | 4 | (function(global) { 5 | let fakeNavigator = {}; 6 | for (let i in global.navigator) { 7 | fakeNavigator[i] = global.navigator[i]; 8 | } 9 | global.navigator = fakeNavigator; 10 | }(window)); 11 | 12 | var goOffline = function goOffline() { 13 | return new RSVP.Promise(resolve => { 14 | window.navigator.__defineGetter__('onLine', function() { 15 | return false; 16 | }); 17 | $(window).trigger('offline'); 18 | resolve(); 19 | }); 20 | }; 21 | var goOnline = function goOnline() { 22 | window.navigator.__defineGetter__('onLine', function() { 23 | return true; 24 | }); 25 | $(window).trigger('online'); 26 | }; 27 | 28 | export { goOnline, goOffline }; 29 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember/resolver'; 2 | import config from '../../config/environment'; 3 | 4 | var resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Application from '../../app'; 3 | import Router from '../../router'; 4 | import config from '../../config/environment'; 5 | import localforageHelpers from './localforage-helpers'; 6 | 7 | export default function startApp(attrs) { 8 | var application; 9 | 10 | var attributes = Ember.merge({}, config.APP); 11 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 12 | 13 | Ember.run(function() { 14 | application = Application.create(attributes); 15 | application.setupForTesting(); 16 | application.injectTestHelpers(); 17 | }); 18 | 19 | return application; 20 | } 21 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for 'head'}} 11 | {{content-for 'test-head'}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for 'head-footer'}} 18 | {{content-for 'test-head-footer'}} 19 | 20 | 21 | 22 | {{content-for 'body'}} 23 | {{content-for 'test-body'}} 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for 'body-footer'}} 31 | {{content-for 'test-body-footer'}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/api-hogs/ember-data-offline/920597742e88d6961ce7cf34332d308a458dc533/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/jobs/localstorage-test.js: -------------------------------------------------------------------------------- 1 | /* global start */ 2 | /* global stop */ 3 | 4 | import Ember from 'ember'; 5 | import { module, test } from 'qunit'; 6 | import { localstorageJobMock } from '../../helpers/job'; 7 | 8 | const { RSVP } = Ember; 9 | 10 | var subject; 11 | 12 | module('Unit | Job | Localstorage', { 13 | beforeEach: function(){ 14 | }, 15 | afterEach: function(){ 16 | } 17 | }); 18 | 19 | 20 | test('#find call adapter #createRecord', function(assert) { 21 | assert.expect(2); 22 | 23 | let job = localstorageJobMock(assert, null,{name : 'find', args : 'foo'}); 24 | 25 | stop(); 26 | job.perform().then(() => { 27 | assert.ok(true,"job.perform"); 28 | start(); 29 | }); 30 | }); 31 | 32 | test('#find pass when there is record in store', function(assert) { 33 | assert.expect(2); 34 | //in store : {id:foo} -> assert for createRecord + job.perform 35 | let job = localstorageJobMock(assert, null, {name : 'find', args : 'foo'}); 36 | 37 | stop(); 38 | job.perform().then(() => { 39 | assert.ok(true,"job.perform"); 40 | start(); 41 | }); 42 | }); 43 | 44 | test('#find pass when there is no record in store', function(assert) { 45 | assert.expect(1); 46 | //in store : {id:foo} -> assert for job.perform only 47 | let job = localstorageJobMock(assert, null, {name : 'find', args : 1}); 48 | 49 | stop(); 50 | job.perform().then(() => { 51 | assert.ok(true,"job.perform"); 52 | start(); 53 | }); 54 | }); 55 | 56 | test('#findAll pass', function(assert) { 57 | assert.expect(3); 58 | //in store : {id:foo} -> assert for createRecord + job.perform 59 | let job = localstorageJobMock(assert, null, {name : 'findAll'}); 60 | 61 | stop(); 62 | job.perform().then(() => { 63 | assert.ok(true,"job.perform"); 64 | start(); 65 | }); 66 | }); 67 | 68 | 69 | test('#findMany pass ', function(assert) { 70 | assert.expect(3); 71 | //in store : {id:foo} -> assert for createRecord + job.perform 72 | let job = localstorageJobMock(assert, null, {name : 'findMany', args : ['foo', 'foo2']}); 73 | 74 | stop(); 75 | job.perform().then(() => { 76 | assert.ok(true,"job.perform"); 77 | start(); 78 | }); 79 | }); 80 | 81 | 82 | test('adapter #updateRecord is successfully by offline job #deleteRecord', function(assert){ 83 | assert.expect(2); 84 | 85 | let job = localstorageJobMock(assert, RSVP.Promise.resolve({id:1}), { name : 'updateRecord'}); 86 | 87 | stop(); 88 | job.perform().then(() => { 89 | assert.ok(true,"job.perform"); 90 | start(); 91 | }); 92 | }); 93 | 94 | test('adapter #deleteRecord is successfully invoked by offline job #deleteRecord', function(assert){ 95 | assert.expect(2); 96 | 97 | let job = localstorageJobMock(assert, RSVP.Promise.resolve({id:1}), { name : 'deleteRecord'}); 98 | 99 | stop(); 100 | job.perform().then(() => { 101 | assert.ok(true,"job.perform"); 102 | start(); 103 | }); 104 | }); 105 | 106 | 107 | test('pass unhandled function through localforage to adapter', function(assert){ 108 | assert.expect(2); 109 | let job = localstorageJobMock(assert, RSVP.Promise.resolve({id:1}), { name : 'unhandled'}); 110 | 111 | stop(); 112 | job.perform().then(() => { 113 | assert.ok(true,"job.perform"); 114 | start(); 115 | }); 116 | }); 117 | 118 | 119 | //test('#findQuery pass', function(assert) { 120 | // assert.expect(2); 121 | //in store : {id:foo} -> assert for createRecord + job.perform 122 | // let job = mockLocastorageJob(assert, RSVP.Promise.resolve({ bar: {id : 'foo', name :'foo'}}), {name : 'findQuery', args: {name : 'foo'}}); 123 | 124 | // stop(); 125 | // job.perform().then(() => { 126 | // assert.ok(true); 127 | // start(); 128 | // }); 129 | // }); 130 | 131 | 132 | // test('#find persists when there is record from online storage that absent in offline', function(assert) { 133 | // assert.expect(2); 134 | 135 | // let job = mockLocastorageJob(RSVP.Promise.resolve(null), RSVP.Promise.resolve({bar: {id: 'foo'}}), assert); 136 | 137 | // stop(); 138 | // job.perform().then(() => { 139 | // assert.ok(true); 140 | // start(); 141 | // }); 142 | // }); 143 | 144 | // test('#find pass when empty response from online', function(assert) { 145 | // assert.expect(1); 146 | 147 | // let job = mockLocastorageJob(RSVP.Promise.resolve(null), null, assert); 148 | 149 | // stop(); 150 | // job.perform().then(() => { 151 | // assert.ok(true); 152 | // start(); 153 | // }); 154 | // }); 155 | 156 | // test('#find pass when error in offline and no online record', function(assert) { 157 | // assert.expect(1); 158 | 159 | // let job = mockLocastorageJob(RSVP.Promise.reject(), RSVP.Promise.resolve(null), assert); 160 | 161 | // stop(); 162 | // job.perform().then(() => { 163 | // assert.ok(true); 164 | // start(); 165 | // }); 166 | // }); 167 | 168 | // test('#find persists when error in offline and found online record', function(assert) { 169 | // assert.expect(2); 170 | 171 | // let job = mockLocastorageJob(RSVP.Promise.reject(), RSVP.Promise.resolve({bar: {id: 'foo'}}), assert); 172 | 173 | // stop(); 174 | // job.perform().then(() => { 175 | // assert.ok(true); 176 | // start(); 177 | // }); 178 | // }); 179 | 180 | // test('#findAll persists when there are online records', function(assert) { 181 | // assert.expect(2); 182 | 183 | // let job = mockLocastorageJob(null, RSVP.Promise.resolve({id: 'foo'}), assert, 'findAll'); 184 | 185 | // stop(); 186 | // job.perform().then(() => { 187 | // assert.ok(true); 188 | // start(); 189 | // }); 190 | // }); 191 | 192 | // test('#findAll pass when there are not online records', function(assert) { 193 | // assert.expect(1); 194 | 195 | // let job = mockLocastorageJob(null, RSVP.Promise.resolve(null), assert, 'findAll'); 196 | 197 | // stop(); 198 | // job.perform().then(() => { 199 | // assert.ok(true); 200 | // start(); 201 | // }); 202 | // }); 203 | 204 | // test('#findQuery pass when there is record from offline storage', function(assert) { 205 | // assert.expect(1); 206 | 207 | // let job = mockLocastorageJob(RSVP.Promise.resolve({id: 2}), null, assert, 'findQuery'); 208 | 209 | // stop(); 210 | // job.perform().then(() => { 211 | // assert.ok(true); 212 | // start(); 213 | // }); 214 | // }); 215 | 216 | // test('#findQuery persists when there is record from online storage that absent in offline', function(assert) { 217 | // assert.expect(2); 218 | 219 | // let job = mockLocastorageJob(RSVP.Promise.resolve(null), {id: 'foo'}, assert, 'findQuery'); 220 | 221 | // stop(); 222 | // job.perform().then(() => { 223 | // assert.ok(true); 224 | // start(); 225 | // }); 226 | // }); 227 | 228 | // test('#findQuery pass when empty response from online', function(assert) { 229 | // assert.expect(1); 230 | 231 | // let job = mockLocastorageJob(RSVP.Promise.resolve(null), null, assert, 'findQuery'); 232 | 233 | // stop(); 234 | // job.perform().then(() => { 235 | // assert.ok(true); 236 | // start(); 237 | // }); 238 | // }); 239 | 240 | // test('#findQuery pass when error in offline and no online record', function(assert) { 241 | // assert.expect(1); 242 | 243 | // let job = mockLocastorageJob(RSVP.Promise.reject(), RSVP.Promise.resolve(null), assert, 'findQuery'); 244 | 245 | // stop(); 246 | // job.perform().then(() => { 247 | // assert.ok(true); 248 | // start(); 249 | // }); 250 | // }); 251 | 252 | // test('#findQuery persists when error in offline and found online record', function(assert) { 253 | // assert.expect(2); 254 | 255 | // let job = mockLocastorageJob(RSVP.Promise.reject(), RSVP.Promise.resolve({id: 'foo'}), assert, 'findQuery'); 256 | 257 | // stop(); 258 | // job.perform().then(() => { 259 | // assert.ok(true); 260 | // start(); 261 | // }); 262 | // }); 263 | 264 | // test('#findMany pass when there is record from offline storage', function(assert) { 265 | // assert.expect(1); 266 | 267 | // let job = mockLocastorageJob(RSVP.Promise.resolve({id: 2}), null, assert, 'findMany'); 268 | 269 | // stop(); 270 | // job.perform().then(() => { 271 | // assert.ok(true); 272 | // start(); 273 | // }); 274 | // }); 275 | 276 | // test('#findMany persists when there is record from online storage that absent in offline', function(assert) { 277 | // assert.expect(2); 278 | 279 | // let job = mockLocastorageJob(RSVP.Promise.resolve(null), {id: 'foo'}, assert, 'findMany'); 280 | 281 | // stop(); 282 | // job.perform().then(() => { 283 | // assert.ok(true); 284 | // start(); 285 | // }); 286 | // }); 287 | 288 | // test('#findMany pass when empty response from online', function(assert) { 289 | // assert.expect(1); 290 | 291 | // let job = mockLocastorageJob(RSVP.Promise.resolve(null), null, assert, 'findMany'); 292 | 293 | // stop(); 294 | // job.perform().then(() => { 295 | // assert.ok(true); 296 | // start(); 297 | // }); 298 | // }); 299 | 300 | // test('#findMany pass when error in offline and no online record', function(assert) { 301 | // assert.expect(1); 302 | 303 | // let job = mockLocastorageJob(RSVP.Promise.reject(), RSVP.Promise.resolve(null), assert, 'findMany'); 304 | 305 | // stop(); 306 | // job.perform().then(() => { 307 | // assert.ok(true); 308 | // start(); 309 | // }); 310 | // }); 311 | 312 | // test('#findMany persists when error in offline and found online record', function(assert) { 313 | // assert.expect(2); 314 | 315 | // let job = mockLocastorageJob(RSVP.Promise.reject(), RSVP.Promise.resolve({id: 'foo'}), assert, 'findMany'); 316 | 317 | // stop(); 318 | // job.perform().then(() => { 319 | // assert.ok(true); 320 | // start(); 321 | // }); 322 | // }); 323 | -------------------------------------------------------------------------------- /tests/unit/jobs/rest-test.js: -------------------------------------------------------------------------------- 1 | /* global start */ 2 | /* global stop */ 3 | 4 | import Ember from 'ember'; 5 | import { module, test } from 'qunit'; 6 | import { restJobMock } from '../../helpers/job'; 7 | 8 | const { RSVP } = Ember; 9 | 10 | var subject; 11 | 12 | module('Unit | Job | REST', { 13 | beforeEach: function(){ 14 | }, 15 | afterEach: function(){ 16 | } 17 | }); 18 | 19 | test('rest job #find call adapter #find', function(assert) { 20 | assert.expect(4); 21 | //adapter.find was invoked. 22 | //store.pushPayload was invoked 23 | //job.perform 24 | //adapter.createOfflineJob was invoked. 25 | 26 | let job = restJobMock(assert, {name : 'find', args : 'foo'}); 27 | 28 | stop(); 29 | job.perform().then(() => { 30 | assert.ok(true,"job.perform"); 31 | start(); 32 | }); 33 | }); 34 | 35 | test('rest job #findAll call adapter #findAll', function(assert) { 36 | assert.expect(4); 37 | //adapter.find was invoked. 38 | //store.pushPayload was invoked 39 | //job.perform 40 | //adapter.createOfflineJob was invoked. 41 | 42 | let job = restJobMock(assert, {name : 'find'}); 43 | 44 | stop(); 45 | job.perform().then(() => { 46 | assert.ok(true,"job.perform"); 47 | start(); 48 | }); 49 | }); 50 | 51 | test('rest job #findAll call adapter #findAll', function(assert) { 52 | assert.expect(4); 53 | 54 | let job = restJobMock(assert, {name : 'findAll'}); 55 | 56 | stop(); 57 | job.perform().then(() => { 58 | assert.ok(true,"job.perform"); 59 | start(); 60 | }); 61 | }); 62 | 63 | test('rest job #findQuery call adapter #findQuery ', function(assert) { 64 | assert.expect(3); 65 | 66 | let job = restJobMock(assert, {name : 'findQuery', args : { name : 'foo'}}); 67 | 68 | stop(); 69 | job.perform().then(() => { 70 | assert.ok(true,"job.perform"); 71 | start(); 72 | }); 73 | }); 74 | 75 | test('rest job #findMany call adapter #findMany', function(assert) { 76 | assert.expect(3); 77 | 78 | let job = restJobMock(assert, {name : 'findMany', args : { name : 'foo', args: [1,2]}}); 79 | 80 | stop(); 81 | job.perform().then(() => { 82 | assert.ok(true,"job.perform"); 83 | start(); 84 | }); 85 | }); 86 | 87 | test('rest job #createRecord pass', function(assert) { 88 | assert.expect(6); 89 | 90 | let job = restJobMock(assert, {name : 'createRecord'}); 91 | 92 | stop(); 93 | job.perform().then(() => { 94 | assert.ok(true,"job.perform"); 95 | start(); 96 | }); 97 | }); 98 | 99 | test('rest job #updateRecord pass', function(assert) { 100 | assert.expect(2); 101 | 102 | let job = restJobMock(assert, {name : 'updateRecord'}); 103 | 104 | stop(); 105 | job.perform().then(() => { 106 | assert.ok(true,"job.perform"); 107 | start(); 108 | }); 109 | }); 110 | 111 | test('rest job #deleteRecord pass', function(assert) { 112 | assert.expect(2); 113 | 114 | let job = restJobMock(assert, {name : 'updateRecord'}); 115 | 116 | stop(); 117 | job.perform().then(() => { 118 | assert.ok(true,"job.perform"); 119 | start(); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /tests/unit/mixins/base-test.js: -------------------------------------------------------------------------------- 1 | /* global start */ 2 | /* global stop */ 3 | 4 | import Ember from 'ember'; 5 | import BaseMixin from 'ember-data-offline/mixins/base'; 6 | import { module, test } from 'qunit'; 7 | 8 | const { RSVP } = Ember; 9 | 10 | var subject; 11 | 12 | var queueMock = Ember.Object.extend({ 13 | add() { 14 | this.get('assert').ok(true, 'queue.add was invoked @' + this.get('encapsulatedIn')); 15 | } 16 | }); 17 | 18 | var storeMock = Ember.Object.extend({ 19 | lookupAdapter() { 20 | return Ember.Object.create({}); 21 | } 22 | }); 23 | 24 | module('Unit | Mixin | Base', { 25 | beforeEach: function() { 26 | subject = Ember.Object.createWithMixins(BaseMixin, { }); 27 | }, 28 | afterEach: function() { 29 | subject = null; 30 | } 31 | }); 32 | 33 | test('#addToQueue with queue @ store ', (assert) => { 34 | assert.expect(1); 35 | 36 | let store = storeMock.create({ 37 | EDOQueue: queueMock.create({ 38 | assert: assert, 39 | encapsulatedIn: 'store' 40 | }) 41 | }); 42 | 43 | let job = Ember.Object.create({}); 44 | subject.addToQueue(job, store, null); 45 | }); 46 | 47 | test('#addToQueue with queue @ baseMixin ', (assert) => { 48 | assert.expect(1); 49 | 50 | let store = storeMock.create({}); 51 | 52 | let job = Ember.Object.create({}); 53 | 54 | subject.reopen({ 55 | EDOQueue: queueMock.create({ 56 | assert: assert, 57 | encapsulatedIn: 'baseMixin' 58 | }) 59 | }); 60 | 61 | subject.addToQueue(job, store, null); 62 | }); 63 | 64 | 65 | test('creating jobs @ baseMixin ', (assert) => { 66 | assert.expect(2); 67 | 68 | let store = storeMock.create({ 69 | EDOQueue: queueMock.create({ 70 | assert: assert, 71 | encapsulatedIn: 'store' 72 | }) 73 | }); 74 | 75 | subject.createOnlineJob("find", [store, {modelName : 'bar'}, null, null], null); 76 | subject.createOfflineJob("find", [store, {modelName : 'bar'}, null, null], store); 77 | }); 78 | -------------------------------------------------------------------------------- /tests/unit/mixins/job-test.js: -------------------------------------------------------------------------------- 1 | /* global start */ 2 | /* global stop */ 3 | 4 | import Ember from 'ember'; 5 | import JobMixin from 'ember-data-offline/mixins/job'; 6 | import { module, test } from 'qunit'; 7 | 8 | const { RSVP } = Ember; 9 | 10 | var subject; 11 | 12 | module('Unit | Mixin | Job', { 13 | beforeEach: function(){ 14 | subject = Ember.Object.createWithMixins(JobMixin, { 15 | retryCount: 1, 16 | }); 17 | }, 18 | afterEach: function(){ 19 | subject = null; 20 | } 21 | }); 22 | 23 | test('it works', function(assert) { 24 | assert.expect(1); 25 | assert.ok(subject); 26 | }); 27 | 28 | test('it computes needRetry', function(assert) { 29 | assert.expect(2); 30 | assert.equal(subject.get('needRetry'), true, 'needRetry true when retryCount > 0'); 31 | subject.decrementProperty('retryCount'); 32 | assert.equal(subject.get('needRetry'), false, 'needRetry false when retryCount == 0'); 33 | }); 34 | 35 | test('#perform returns resolving Promise on default', function(assert) { 36 | assert.expect(1); 37 | 38 | stop(); 39 | subject.perform().then(() => { 40 | assert.ok(true, 'perform always resolves'); 41 | start(); 42 | }); 43 | }); 44 | 45 | test('#perform runs task function', function(assert) { 46 | assert.expect(6); 47 | 48 | let passThisTest = function() { 49 | assert.ok(true, 'calls from success task'); 50 | return true; 51 | }; 52 | let successJob = Ember.Object.createWithMixins(JobMixin, { 53 | task: passThisTest, 54 | }); 55 | 56 | stop(); 57 | successJob.perform().then(() => { 58 | assert.ok(true, 'perform always resolves'); 59 | start(); 60 | }); 61 | 62 | let failedTask = function() { 63 | assert.ok(true, 'calls from failing task'); 64 | return RSVP.Promise.reject(); 65 | }; 66 | let failJob = Ember.Object.createWithMixins(JobMixin, { 67 | task: failedTask, 68 | }); 69 | 70 | stop(); 71 | failJob.perform().catch(() => { 72 | assert.ok(true, 'task fails'); 73 | start(); 74 | }); 75 | 76 | let returnValue = function() { 77 | assert.ok(true, 'calls from value task'); 78 | return RSVP.Promise.resolve('value'); 79 | }; 80 | let returnValueJob = Ember.Object.createWithMixins(JobMixin, { 81 | task: returnValue, 82 | }); 83 | 84 | stop(); 85 | returnValueJob.perform().then(val => { 86 | assert.equal('value', val, 'values are equal'); 87 | start(); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /tests/unit/mixins/offline-test.js: -------------------------------------------------------------------------------- 1 | /* global start */ 2 | /* global stop */ 3 | 4 | import Ember from 'ember'; 5 | import OfflineMixin from 'ember-data-offline/mixins/offline'; 6 | import { module, test } from 'qunit'; 7 | import { 8 | getStoreMock, getQueueMock, getSnapshotMock, getAdapterMock, 9 | getTypeMock, getResultMock, getResultFromPayloadMock 10 | } from '../../helpers/base'; 11 | 12 | var subject, store, typeClass, snapshot, expectedResult, expectedResultFromPayload; 13 | 14 | store = getStoreMock(); 15 | typeClass = getTypeMock(); 16 | 17 | module('Unit | Mixin | offline', { 18 | beforeEach: function(){ 19 | 20 | snapshot = getSnapshotMock(); 21 | expectedResult = getResultMock(); 22 | expectedResultFromPayload = getResultFromPayloadMock(); 23 | 24 | let offlineAdapterMock = getAdapterMock("OFFLINE"); 25 | 26 | subject = Ember.Object.extend(offlineAdapterMock,{ 27 | onlineAdapter : Ember.Object.create({ 28 | //assert : this.get('assert'), 29 | findQuery(){ 30 | this.get('assert').ok(true, 'query was invoked @ online adapter'); 31 | return Ember.RSVP.Promise.resolve({ bar: Ember.A([expectedResultFromPayload])}); 32 | } 33 | }) 34 | }).extend(OfflineMixin).create(); 35 | }, 36 | afterEach: function(){ 37 | subject = null; 38 | snapshot = null; 39 | expectedResult = null; 40 | expectedResultFromPayload = null; 41 | } 42 | }); 43 | 44 | 45 | 46 | test('findAll', (assert)=>{ 47 | assert.expect(5); 48 | subject.set('assert', assert); 49 | 50 | //2 asserts : adapter.findAll + equal 51 | stop(); 52 | subject.findAll(store, typeClass, 'sinceToken', [snapshot], true).then((results)=>{ 53 | assert.equal(results[0].name, expectedResult.name); 54 | start(); 55 | }); 56 | 57 | subject.set('EDOQueue', getQueueMock(assert, 'offlineMixin')); 58 | //2 asserts : adapter.findAll + createOnlineJob + equal 59 | stop(); 60 | subject.findAll(store, typeClass, 'sinceToken', [snapshot], false).then((results)=>{ 61 | assert.equal(results[0].name, expectedResult.name); 62 | start(); 63 | }); 64 | }); 65 | 66 | test('find', (assert)=>{ 67 | assert.expect(7); 68 | 69 | subject.set('assert', assert); 70 | 71 | //2 asserts : adapter.find + equal 72 | stop(); 73 | subject.find(store, typeClass, 'foo', snapshot, true).then((result)=>{ 74 | assert.equal(result.name, expectedResult.name); 75 | start(); 76 | }); 77 | 78 | //2 asserts : adapter.find + equal 79 | stop(); 80 | subject.find(store, typeClass, 'no_record', snapshot, true).then((result)=>{ 81 | assert.equal(result.id, 'no_record'); 82 | start(); 83 | }); 84 | 85 | subject.set('EDOQueue', getQueueMock(assert, 'offlineMixin')); 86 | //3 asserts : adapter.find + createOnlineJob + equal 87 | stop(); 88 | subject.find(store, typeClass, 'foo', snapshot, false).then((result)=>{ 89 | assert.equal(result.name, expectedResult.name); 90 | start(); 91 | }); 92 | }); 93 | 94 | test('query', (assert)=>{ 95 | assert.expect(8); 96 | 97 | subject.set('assert', assert); 98 | 99 | //2 asserts : adapter.query + equal 100 | stop(); 101 | subject.query(store, typeClass, {name : 'foo'}, [snapshot], true).then((results)=>{ 102 | assert.equal(results[0].name, expectedResult.name); 103 | start(); 104 | }); 105 | 106 | subject.get('onlineAdapter').set('assert', assert); 107 | 108 | //3 asserts : adapter.query + onlineAdapter.findQuery + equal 109 | stop(); 110 | subject.query(store, typeClass, 'no_record', [snapshot], true).then((results)=>{ 111 | assert.equal(results[0].name, expectedResultFromPayload.name); 112 | start(); 113 | }); 114 | 115 | subject.set('EDOQueue', getQueueMock(assert, 'offlineMixin')); 116 | //3 asserts : adapter.query + createOnlineJob + equal 117 | stop(); 118 | subject.query(store, typeClass, {name : 'foo'}, [snapshot], false).then((results)=>{ 119 | assert.equal(results[0].name, expectedResult.name); 120 | start(); 121 | }); 122 | 123 | }); 124 | 125 | 126 | test('findMany', (assert)=>{ 127 | assert.expect(7); 128 | 129 | subject.set('assert', assert); 130 | 131 | //2 asserts : adapter.findMany + equal 132 | stop(); 133 | subject.findMany(store, typeClass, ['foo'], [snapshot], true).then((results)=>{ 134 | assert.equal(results[0].name, expectedResult.name); 135 | start(); 136 | }); 137 | 138 | //2 asserts : adapter.findMany + equal 139 | stop(); 140 | subject.findMany(store, typeClass, ['no_record'], [snapshot], true).then((results)=>{ 141 | assert.equal(results[0].id, 'no_record', "returns stub"); 142 | start(); 143 | }); 144 | 145 | subject.set('EDOQueue', getQueueMock(assert, 'offlineMixin')); 146 | //3 asserts : adapter.findMany + createOnlineJob + equal 147 | stop(); 148 | subject.findMany(store, typeClass, ['foo'], [snapshot], false).then((results)=>{ 149 | assert.equal(results[0].name, expectedResult.name); 150 | start(); 151 | }); 152 | }); 153 | 154 | 155 | test('createRecord', (assert) => { 156 | assert.expect(5); 157 | 158 | subject.set('assert', assert); 159 | 160 | //2 asserts : adapter.createRecord + equal 161 | stop(); 162 | subject.createRecord(store, typeClass, snapshot, true).then((result) => { 163 | assert.ok(result); 164 | start(); 165 | }); 166 | 167 | //3 asserts : adapter.createRecord + queue.add + equal 168 | subject.set('EDOQueue', getQueueMock(assert, 'oflineMixin')); 169 | stop(); 170 | subject.createRecord(store, typeClass, snapshot, false).then((result) => { 171 | assert.ok(result); 172 | start(); 173 | }); 174 | }); 175 | 176 | test('updateRecord', (assert) => { 177 | assert.expect(5); 178 | 179 | subject.set('assert', assert); 180 | 181 | //2 asserts : adapter.updateRecord + equal 182 | stop(); 183 | subject.updateRecord(store, typeClass, snapshot, true).then((result) => { 184 | assert.ok(result); 185 | start(); 186 | }); 187 | 188 | //3 asserts : adapter.updateRecord + queue.add + equal 189 | subject.set('EDOQueue', getQueueMock(assert, 'oflineMixin')); 190 | stop(); 191 | subject.updateRecord(store, typeClass, snapshot, false).then((result) => { 192 | assert.ok(result); 193 | start(); 194 | }); 195 | }); 196 | 197 | test('deleteRecord', (assert) => { 198 | assert.expect(5); 199 | 200 | subject.set('assert', assert); 201 | 202 | stop(); 203 | subject.deleteRecord(store, typeClass, snapshot, true).then((result) => { 204 | assert.ok(result); 205 | start(); 206 | }); 207 | 208 | //3 asserts : adapter.deleteRecord + queue.add + equal 209 | subject.set('EDOQueue', getQueueMock(assert, 'oflineMixin')); 210 | stop(); 211 | subject.deleteRecord(store, typeClass, snapshot, false).then((result) => { 212 | assert.ok(result); 213 | start(); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /tests/unit/mixins/online-test.js: -------------------------------------------------------------------------------- 1 | /* global start */ 2 | /* global stop */ 3 | 4 | import Ember from 'ember'; 5 | import OnlineMixin from 'ember-data-offline/mixins/online'; 6 | import { module, test } from 'qunit'; 7 | import { 8 | getStoreMock, getQueueMock, getSnapshotMock, getAdapterMock, 9 | getTypeMock, getResultMock, getResultFromPayloadMock 10 | } from '../../helpers/base'; 11 | 12 | var subject, store, typeClass, snapshot, expectedResult, expectedResultFromPayload; 13 | 14 | store = getStoreMock(); 15 | typeClass = getTypeMock(); 16 | 17 | module('Unit | Mixin | online', { 18 | beforeEach: function(){ 19 | 20 | snapshot = getSnapshotMock(); 21 | expectedResult = getResultMock(); 22 | expectedResultFromPayload = getResultFromPayloadMock(); 23 | 24 | let onlineAdapterMock = getAdapterMock("ONLINE"); 25 | subject = Ember.Object.extend(onlineAdapterMock).extend(OnlineMixin).create(); 26 | }, 27 | afterEach: function(){ 28 | subject = null; 29 | snapshot = null; 30 | expectedResult = null; 31 | expectedResultFromPayload = null; 32 | } 33 | }); 34 | 35 | test('findAll', (assert)=>{ 36 | assert.expect(2); 37 | 38 | subject.set('assert', assert); 39 | 40 | //asserts 2: findAll + equal 41 | stop(); 42 | subject.findAll(store, typeClass).then((records)=>{ 43 | assert.equal(records[0].name,expectedResult.name); 44 | start(); 45 | }); 46 | }); 47 | 48 | test('find', (assert)=>{ 49 | assert.expect(4); 50 | 51 | subject.set('assert',assert); 52 | 53 | store.set('isOfflineEnabled',false); 54 | //asserts 2: find call + equal 55 | stop(); 56 | subject.find(store, typeClass, 'foo', snapshot, true).then((record)=>{ 57 | assert.equal(record.name,expectedResult.name); 58 | start(); 59 | }); 60 | 61 | subject.set('EDOQueue', getQueueMock(assert,'onlineMixin')); 62 | store.set('isOfflineEnabled',true); 63 | //asserts 2: find call + equal 64 | stop(); 65 | subject.find(store, typeClass, 'foo', snapshot, false).then((record)=>{ 66 | assert.equal(record.name,expectedResult.name); 67 | start(); 68 | }); 69 | }); 70 | 71 | test('findQuery', (assert)=>{ 72 | assert.expect(5); 73 | 74 | subject.set('assert',assert); 75 | 76 | store.set('isOfflineEnabled',false); 77 | //asserts 2: findQuery call + equal 78 | stop(); 79 | subject.findQuery(store, typeClass,{name: 'foo'}, [snapshot], true).then((records)=>{ 80 | assert.equal(records[0].name,expectedResult.name); 81 | start(); 82 | }); 83 | 84 | subject.set('EDOQueue', getQueueMock(assert,'onlineMixin')); 85 | store.set('isOfflineEnabled',true); 86 | //asserts 3: findQuery call + queue.add + equal 87 | stop(); 88 | subject.findQuery(store, typeClass, {name: 'foo'}, [snapshot], false).then((records)=>{ 89 | assert.equal(records[0].name,expectedResult.name); 90 | start(); 91 | }); 92 | }); 93 | 94 | test('findMany', (assert)=>{ 95 | assert.expect(5); 96 | 97 | subject.set('assert', assert); 98 | store.set('isOfflineEnabled',true); 99 | //2 asserts : findMany + equal 100 | stop(); 101 | subject.findMany(store, typeClass, ['foo'], [snapshot], true).then((result)=>{ 102 | assert.equal(result[0].name, expectedResult.name); 103 | start(); 104 | }); 105 | 106 | subject.set('EDOQueue', getQueueMock(assert,'onlineMixin')); 107 | store.set('isOfflineEnabled',true); 108 | //asserts 3: findMany call + queue.add + equal 109 | stop(); 110 | subject.findMany(store, typeClass, ['foo'], [snapshot], false).then((records)=>{ 111 | assert.equal(records[0].name,expectedResult.name); 112 | start(); 113 | }); 114 | 115 | }); 116 | 117 | test('create, delete, update operations', (assert)=>{ 118 | assert.expect(6); 119 | 120 | subject.set('assert', assert); 121 | //2 asserts : createRecord + ok 122 | stop(); 123 | subject.createRecord().then((result)=>{ 124 | assert.ok(result); 125 | start(); 126 | }); 127 | //2 asserts : updateRecord + ok 128 | stop(); 129 | subject.updateRecord().then((result)=>{ 130 | assert.ok(result); 131 | start(); 132 | }); 133 | //2 asserts : deleteRecord + ok 134 | stop(); 135 | subject.deleteRecord().then((result)=>{ 136 | assert.ok(result); 137 | start(); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /tests/unit/queue-test.js: -------------------------------------------------------------------------------- 1 | /* global stop, start */ 2 | import Ember from 'ember'; 3 | import Subject from 'ember-data-offline/queue'; 4 | import { module, test } from 'qunit'; 5 | 6 | const { Object: emberObject } = Ember; 7 | 8 | var Job; 9 | Job = Ember.Object.create({needRetry: false, retryCount: 0}); 10 | var queue; 11 | 12 | module('Unit | Queue', { 13 | beforeEach: function(){ 14 | queue = Subject.create({ 15 | retryOnFailureDelay: 0, 16 | delay: 0 17 | }); 18 | }, 19 | afterEach: function(){ 20 | queue = null; 21 | } 22 | }); 23 | 24 | test('base setup success', function(assert) { 25 | assert.expect(5); 26 | assert.ok(queue); 27 | assert.equal(queue.get('activeJobs').length, 0); 28 | assert.equal(queue.get('pendingJobs').length, 0); 29 | assert.equal(queue.get('failureJobs').length, 0); 30 | assert.equal(queue.get('retryJobs').length, 0); 31 | }); 32 | 33 | test('retry job in queue', function(assert){ 34 | assert.expect(2); 35 | let queue = Subject.create({ 36 | delay: 100, 37 | retryOnFailureDelay: 150, 38 | }); 39 | let jobKlass = Ember.Object.extend({ 40 | adapter: Ember.Object.create({}), 41 | perform: function(){ 42 | return Ember.RSVP.Promise.reject(); 43 | } 44 | }); 45 | let job = jobKlass.create({needRetry: true, retryCount: 1}); 46 | queue.add(job); 47 | assert.equal(queue.get('activeJobs').length, 1); 48 | stop(); 49 | Ember.run.later(() => { 50 | assert.equal(queue.get('retryJobs').length, 1); 51 | start(); 52 | }, 150); 53 | }); 54 | 55 | test('fail job in queue', function(assert){ 56 | assert.expect(2); 57 | let queue = Subject.create({ 58 | delay: 100, 59 | retryOnFailureDelay: 150, 60 | }); 61 | let jobKlass = Ember.Object.extend({ 62 | adapter: Ember.Object.create({}), 63 | perform: function(){ 64 | return Ember.RSVP.Promise.reject(); 65 | } 66 | }); 67 | let job = jobKlass.create({needRetry: false, method: 'test', adapter: '1'}); 68 | queue.add(job); 69 | assert.equal(queue.get('activeJobs').length, 1); 70 | stop(); 71 | Ember.run.later(() => { 72 | assert.equal(queue.get('failureJobs').length, 1); 73 | start(); 74 | }, 150); 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/api-hogs/ember-data-offline/920597742e88d6961ce7cf34332d308a458dc533/vendor/.gitkeep -------------------------------------------------------------------------------- /yuidoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-data-offline", 3 | "description": "Ember data extension to allow offline persistance and caching", 4 | "version": "0.0.1", 5 | "options": { 6 | "external": { 7 | "data": [ 8 | { 9 | "base": "http://emberjs.com/api/", 10 | "json": "http://builds.emberjs.com/tags/v1.13.1/ember-docs.json" 11 | }, 12 | { 13 | "base": "http://emberjs.com/api/", 14 | "json": "http://builds.emberjs.com/tags/v1.13.1/ember-data-docs.json" 15 | } 16 | ] 17 | }, 18 | "linkNatives": "true", 19 | "paths": [ 20 | "./addon" 21 | ], 22 | "outdir": "./output" 23 | } 24 | } 25 | --------------------------------------------------------------------------------