├── .gitignore
├── README.md
├── test
├── test.html
└── test.js
├── bower.json
└── backbone.idbdualstorage.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | bower_components
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Backbone IndexedDB DualStorage adapter (don't use in production!)
3 |
4 | Inspired by [Backbone.dualStorage](https://github.com/nilbus/Backbone.dualStorage) but with bigger database capabilities thanks to IndexedDB.
5 |
6 | # Dependencies
7 |
8 | ```json
9 | {
10 | "backbone": "~1.1.2",
11 | "underscore": "~1.7.0",
12 | "indexeddb-backbonejs-adapter": "git://github.com/SonoIo/indexeddb-backbonejs-adapter.git#master",
13 | "jquery": "~2.1.1",
14 | "idb": "~1.0.0"
15 | }
16 | ```
17 |
18 | # To do
19 |
20 | - Documentation
21 | - Add more tests
22 | - Refactoring
23 | - Beautify the code
24 | - Remove the indexeddb-backbonejs-adapter dependency
25 | - Use only one database, not two
26 | - Implementations
27 | - persistent connection to IndexedDB
28 |
29 | # License
30 |
31 | MIT
32 |
33 |
--------------------------------------------------------------------------------
/test/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | DualStorage tests
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Backbone.IDBDualStorage",
3 | "main": "backbone.idbdualstorage.ks",
4 | "version": "0.0.1",
5 | "homepage": "https://github.com/insideabit/Backbone.IDBDualStorage",
6 | "authors": [
7 | "Inside a bit"
8 | ],
9 | "description": "A dual (IndexedDB and REST) sync adapter for Backbone.js",
10 | "moduleType": [
11 | "amd",
12 | "globals",
13 | "node"
14 | ],
15 | "keywords": [
16 | "backbone",
17 | "storage",
18 | "adapter",
19 | "sync",
20 | "indexeddb",
21 | "idb"
22 | ],
23 | "license": "MIT",
24 | "ignore": [
25 | "**/.*",
26 | "node_modules",
27 | "bower_components",
28 | "test",
29 | "tests"
30 | ],
31 | "dependencies": {
32 | "backbone": "~1.1.2",
33 | "underscore": "~1.7.0",
34 | "indexeddb-backbonejs-adapter": "git://github.com/SonoIo/indexeddb-backbonejs-adapter.git#master",
35 | "jquery": "~2.1.1",
36 | "idb": "~1.0.0"
37 | },
38 | "devDependencies": {
39 | "mocha": "~1.21.4",
40 | "chai": "~1.9.2"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 |
2 | var assert = chai.assert;
3 |
4 |
5 | var deleteAllDatabase = function deleteAllDatabase(done) {
6 | setTimeout(function() {
7 | IDB.dropDatabase('testdb', function (err) {
8 | if (err) return done(err);
9 | IDB.dropDatabase('dirtystore', function (err) {
10 | if (err) return done(err);
11 | done();
12 | });
13 | });
14 | }, 200);
15 | };
16 |
17 | var closeAllDatabaseConnections = function closeAllDatabaseConnections(done) {
18 | Backbone.sync('closeall', null, {
19 | success: function() {
20 | done();
21 | },
22 | error: function(err) {
23 | done(err);
24 | }
25 | });
26 | };
27 |
28 | var databaseId = 'testdb';
29 | var database = window.database = {
30 | id: databaseId,
31 | description: "The database for test",
32 | migrations: [{
33 | version: 1,
34 | migrate: function (transaction, next) {
35 | var customers = transaction.db.createObjectStore("customers");
36 | next();
37 | }
38 | }]
39 | };
40 |
41 | var openDB = function openDB(db, done) {
42 | var request = indexedDB.open(db, 1);
43 |
44 | request.onsuccess = function(e) {
45 | var db = e.target.result;
46 | done(null, db);
47 | };
48 | request.onerror = done;
49 | };
50 |
51 | var getItem = function getItem(options, done) {
52 | if (!options.storeName) return done(new Error('Missing storeName'));
53 | if (!options.db) return done(new Error('Missing DB'));
54 |
55 | var db;
56 | var result;
57 |
58 | var handleResponse = function handleResponse(err, result) {
59 | db.close();
60 | done(err, result);
61 | };
62 |
63 | var onReady = function onReady(err, instance) {
64 | if (err) return done(err);
65 |
66 | db = instance;
67 |
68 | var trans = db.transaction([options.storeName], 'readwrite');
69 | var store = trans.objectStore(options.storeName);
70 |
71 | var keyRange;
72 | if (typeof options.key === 'undefined')
73 | keyRange = IDBKeyRange.lowerBound(0);
74 | else
75 | keyRange = IDBKeyRange.only(options.key);
76 |
77 | var cursorRequest = store.openCursor(keyRange);
78 |
79 | cursorRequest.onsuccess = function (e) {
80 | result = e.target.result;
81 | // if (result == false)
82 | // result.continue();
83 | return;
84 | };
85 |
86 | cursorRequest.onerror = handleResponse;
87 |
88 | trans.oncomplete = function() {
89 | if(!!result == false)
90 | return handleResponse();
91 | handleResponse(null, result.value);
92 | };
93 | };
94 |
95 | openDB(options.db, onReady);
96 | };
97 |
98 | var Customer = Backbone.Model.extend({
99 | database: database,
100 | storeName: 'customers',
101 | idAttribute: '_id'
102 | });
103 |
104 | var Customers = Backbone.Collection.extend({
105 | database: database,
106 | storeName: 'customers',
107 | url: '/api/customers',
108 | model: Customer
109 | });
110 |
111 | var Order = Backbone.Model.extend({
112 | database: database,
113 | storeName: 'orders',
114 | idAttribute: '_id'
115 | });
116 |
117 | describe('DirtyStore', function() {
118 |
119 | beforeEach(function (done) {
120 | deleteAllDatabase(done);
121 | });
122 |
123 | afterEach(function (done) {
124 | closeAllDatabaseConnections(done);
125 | });
126 |
127 | it('Reset dirty and destroyed stores', function (done) {
128 |
129 | var customerA_dirty_id;
130 | var customerA_destroyed_id;
131 | var customerA = new Customer({
132 | _id: 'fake-customer-id-1',
133 | firstname: 'Tom',
134 | lastname: 'Smith'
135 | });
136 |
137 | var customerB_dirty_id;
138 | var customerB_destroyed_id;
139 | var customerB = new Customer({
140 | _id: 'fake-customer-id-2',
141 | firstname: 'John',
142 | lastname: 'Johnson'
143 | });
144 |
145 | var orderA_dirty_id;
146 | var orderA_destroyed_id;
147 | var orderA = new Order({
148 | _id: 'fake-order-id-1',
149 | _customerId: 'fake-customer-id-1',
150 | total: 550
151 | });
152 |
153 |
154 | DirtyStore.getInstance(function (err, store) {
155 | if (err) return done(err);
156 |
157 | var finalize = function finalize() {
158 | store.close();
159 | done();
160 | };
161 |
162 | var checkDestroyedStore = function checkDestroyedStore() {
163 | var options = {
164 | key: customerA_destroyed_id,
165 | storeName: 'destroyed',
166 | db: 'dirtystore'
167 | };
168 | getItem(options, function (err, result) {
169 | if (err) return done(err);
170 | assert.isUndefined(result, 'No customer should be saved');
171 | var options = {
172 | key: orderA_destroyed_id,
173 | storeName: 'destroyed',
174 | db: 'dirtystore'
175 | };
176 | getItem(options, function (err, result) {
177 | if (err) return done(err);
178 | console.log(result);
179 | assert.isDefined(result, 'Order should be saved');
180 | assert.equal(result.modelId, 'fake-order-id-1');
181 | finalize();
182 | });
183 | });
184 | };
185 |
186 | var checkDirtyStore = function checkDirtyStore() {
187 | var options = {
188 | key: customerA_dirty_id,
189 | storeName: 'dirty',
190 | db: 'dirtystore'
191 | };
192 | getItem(options, function (err, result) {
193 | if (err) return done(err);
194 | assert.isUndefined(result, 'No customer should be saved');
195 | var options = {
196 | key: orderA_dirty_id,
197 | storeName: 'dirty',
198 | db: 'dirtystore'
199 | };
200 | getItem(options, function (err, result) {
201 | if (err) return done(err);
202 | assert.isDefined(result, 'Order should be saved');
203 | assert.equal(result.modelId, 'fake-order-id-1');
204 | checkDestroyedStore();
205 | });
206 | });
207 | };
208 |
209 | var reset = function reset() {
210 | store.reset(Customer.prototype.storeName, function (err) {
211 | if (err) return done(err);
212 | checkDirtyStore();
213 | });
214 | };
215 |
216 | store.addDirty(customerA, function (err, newId) {
217 | if (err) return done(err);
218 |
219 | customerA_dirty_id = newId;
220 | store.addDirty(customerB, function (err, newId) {
221 | if (err) return done(err);
222 |
223 | customerB_dirty_id = newId;
224 | store.addDirty(orderA, function (err, newId) {
225 | if (err) return done(err);
226 |
227 | orderA_dirty_id = newId;
228 | store.addDestroyed(customerA, function (err, newId) {
229 | if (err) return done(err);
230 |
231 | customerA_destroyed_id = newId;
232 | store.addDestroyed(customerB, function (err, newId) {
233 | if (err) return done(err);
234 |
235 | customerB_destroyed_id = newId;
236 | store.addDestroyed(orderA, function (err, newId) {
237 | if (err) return done(err);
238 | orderA_destroyed_id = newId;
239 | reset();
240 | });
241 | });
242 | });
243 | });
244 | });
245 | });
246 | });
247 | });
248 | });
249 |
250 | describe('Offline mode', function() {
251 |
252 | beforeEach(function (done) {
253 | Backbone.DualStorage.forceOffline = true;
254 | deleteAllDatabase(done);
255 | });
256 |
257 | afterEach(function (done) {
258 | closeAllDatabaseConnections(done);
259 | });
260 |
261 | it('Should create a new customer', function (done) {
262 | var customers = new Customers();
263 | var customer = new Customer();
264 |
265 | customers.add(customer);
266 |
267 | customer.save({
268 | firstname: 'Tom',
269 | lastname: 'Smith',
270 | company: 'ACME',
271 |
272 | vat: '01234567890',
273 | iban: 'IT011C010000000000000012234'
274 | }, {
275 | success: function() {
276 | assert.isNotNull(customer.id, 'model.id should be not null');
277 | done();
278 | },
279 | error: done
280 | });
281 | });
282 |
283 | it('Should update a customer', function (done) {
284 | var customers = new Customers();
285 | var customer = new Customer();
286 |
287 | customers.add(customer);
288 |
289 | var onSuccess = function onSuccess() {
290 | assert.isNotNull(customer.id, 'model.id should be not null');
291 | customer.save({
292 | firstname: 'John',
293 | lastname: 'Johnson'
294 | }, {
295 | success: function(model) {
296 | assert.equal(model.get('firstname'), 'John');
297 | assert.equal(model.get('lastname'), 'Johnson');
298 | assert.equal(model.get('company'), 'ACME');
299 | done();
300 | },
301 | error: done
302 | });
303 | };
304 |
305 | customer.save({
306 | firstname: 'Tom',
307 | lastname: 'Smith',
308 | company: 'ACME',
309 |
310 | vat: '01234567890',
311 | iban: 'IT011C010000000000000012234'
312 | }, {
313 | success: onSuccess,
314 | error: done
315 | });
316 | });
317 |
318 | it('Should delete a customer', function (done) {
319 | var customers = new Customers();
320 | var customer = new Customer();
321 |
322 | customers.add(customer);
323 |
324 | var deleteCustomer = function deleteCustomer(model) {
325 | model.destroy({
326 | success: function() {
327 | assert.equal(customers.length, 0, 'No items in the customers collection');
328 | checkDirtyStore(model);
329 | },
330 | error: done
331 | });
332 | };
333 |
334 | var checkDirtyStore = function checkDirtyStore(model) {
335 | DirtyStore.getInstance(function (err, store) {
336 | store.findDirty(model, function (err, result) {
337 | store.close();
338 | if (err) return done(err);
339 | assert.isUndefined(result, 'Dirty data should be empty');
340 | done();
341 | });
342 | });
343 | };
344 |
345 | customer.save({
346 | firstname: 'Tom',
347 | lastname: 'Smith',
348 | company: 'ACME',
349 |
350 | vat: '01234567890',
351 | iban: 'IT011C010000000000000012234'
352 | }, {
353 | success: deleteCustomer,
354 | error: done
355 | });
356 | });
357 |
358 | it('Should read a collection of customers', function (done) {
359 | var customers = new Customers();
360 | var customerA = new Customer();
361 | var customerB = new Customer();
362 |
363 | customers.add(customerA);
364 | customers.add(customerB);
365 |
366 | var readCustomers = function readCustomers() {
367 | var readedCustomers = new Customers();
368 | readedCustomers.fetch({
369 | success: function(){
370 | assert.equal(readedCustomers.length, 2, 'Customers length should be greater than zero');
371 | assert.equal(readedCustomers.get(customerA.id).get('firstname'), 'Tom');
372 | assert.equal(readedCustomers.get(customerB.id).get('firstname'), 'John');
373 | done();
374 | },
375 | error: done
376 | });
377 | };
378 |
379 | var saveCustomerB = function saveCustomerB() {
380 | customerB.save({
381 | firstname: 'John',
382 | lastname: 'Johnson',
383 | company: 'ACME',
384 | }, {
385 | success: readCustomers,
386 | error: done
387 | });
388 | };
389 |
390 | customerA.save({
391 | firstname: 'Tom',
392 | lastname: 'Smith',
393 | company: 'ACME',
394 | }, {
395 | success: saveCustomerB,
396 | error: done
397 | });
398 | });
399 |
400 | it('Should read a single customer', function (done) {
401 | var customer = new Customers();
402 | var customerA = new Customer();
403 |
404 | customer.add(customerA);
405 |
406 | var fetchCustomer = function fetchCustomer() {
407 | var customerB = new Customer();
408 | customerB.set('_id', 'this-is-a-client-id');
409 | customerB.fetch({
410 | success: function() {
411 | assert.equal(customerB.get('firstname'), 'Tom');
412 | assert.equal(customerB.get('lastname'), 'Smith');
413 | assert.equal(customerB.get('company'), 'ACME');
414 | done();
415 | },
416 | error: done
417 | });
418 | };
419 |
420 | customerA.save({
421 | _id: 'this-is-a-client-id',
422 | firstname: 'Tom',
423 | lastname: 'Smith',
424 | company: 'ACME'
425 | }, {
426 | success: function() {
427 | assert.isNotNull(customerA.id);
428 | fetchCustomer();
429 | },
430 | error: done
431 | });
432 | });
433 |
434 | });
435 |
436 |
437 | describe('Online mode', function() {
438 |
439 | var _onlineSync = Backbone.onlineSync;
440 |
441 | before(function (done) {
442 | Backbone.DualStorage.isOnline = true;
443 | done();
444 | });
445 |
446 | beforeEach(function (done) {
447 | deleteAllDatabase(done)
448 | });
449 |
450 | afterEach(function (done) {
451 | Backbone.onlineSync = _onlineSync;
452 | closeAllDatabaseConnections(done);
453 | });
454 |
455 | it('Should create a new customer', function (done) {
456 | Backbone.onlineSync = function (method, model, options) {
457 | options.success({
458 | _id: 'this-is-the-server-id'
459 | });
460 | };
461 |
462 | var checkDirtyStore = function() {
463 | var options = {
464 | key: 'this-is-the-server-id',
465 | storeName: 'customers',
466 | db: databaseId
467 | };
468 | getItem(options, function (err, result) {
469 | if (err) return done(err);
470 | assert.isNotNull(result, 'Customer should be saved on DB');
471 | assert.equal(result._id, 'this-is-the-server-id', 'result._id should filled by the server');
472 | assert.equal(result.firstname, 'Tom');
473 |
474 | var options = {
475 | storeName: 'dirty',
476 | db: 'dirtystore'
477 | };
478 | getItem(options, function (err, result) {
479 | if (err) return done(err);
480 | assert.isUndefined(result, 'Dirty store should be empty');
481 | done();
482 | });
483 | });
484 | };
485 |
486 | var customers = new Customers();
487 | var customer = new Customer();
488 |
489 | customers.add(customer);
490 | customer.save({
491 | firstname: 'Tom',
492 | lastname: 'Smith',
493 | company: 'ACME',
494 | vat: '01234567890',
495 | iban: 'IT011C010000000000000012234'
496 | }, {
497 | success: function() {
498 | assert.equal(customer.id, 'this-is-the-server-id', 'model.id should be filled by the server');
499 | checkDirtyStore();
500 | },
501 | error: done
502 | });
503 | });
504 |
505 | it('Should update a customer', function (done) {
506 | Backbone.onlineSync = function (method, model, options) {
507 | options.success({
508 | _id: 'this-is-the-server-id'
509 | });
510 | };
511 |
512 | var customers = new Customers();
513 | var customer = new Customer();
514 |
515 | customers.add(customer);
516 |
517 | var updateCustomer = function updateCustomer() {
518 | assert.isNotNull(customer.id, 'model.id should be not null');
519 | customer.save({
520 | firstname: 'John',
521 | lastname: 'Johnson'
522 | }, {
523 | success: function(model) {
524 | assert.equal(model.get('firstname'), 'John', 'Should update firstname');
525 | assert.equal(model.get('lastname'), 'Johnson', 'Should update lastname');
526 | assert.equal(model.get('company'), 'ACME', 'Should update company');
527 | checkDirtyStore();
528 | },
529 | error: done
530 | });
531 | };
532 |
533 | var checkDirtyStore = function checkDirtyStore() {
534 | var options = {
535 | key: 'this-is-the-server-id',
536 | storeName: 'customers',
537 | db: databaseId
538 | };
539 | getItem(options, function (err, result) {
540 | if (err) return done(err);
541 | assert.isNotNull(result, 'Customer should be saved on DB');
542 | assert.equal(result._id, 'this-is-the-server-id', 'result._id should filled by the server');
543 | assert.equal(result.firstname, 'John', 'Should store the new firstname');
544 | assert.equal(result.lastname, 'Johnson', 'Should store the new lastname');
545 |
546 | var options = {
547 | storeName: 'dirty',
548 | db: 'dirtystore'
549 | };
550 | getItem(options, function (err, result) {
551 | if (err) return done(err);
552 | assert.isUndefined(result, 'Dirty store should be empty');
553 | done();
554 | });
555 | });
556 | };
557 |
558 | customer.save({
559 | firstname: 'Tom',
560 | lastname: 'Smith',
561 | company: 'ACME',
562 |
563 | vat: '01234567890',
564 | iban: 'IT011C010000000000000012234'
565 | }, {
566 | success: updateCustomer,
567 | error: done
568 | });
569 | });
570 |
571 | it('Should delete a customer', function (done) {
572 | Backbone.onlineSync = function (method, model, options) {
573 | options.success({
574 | _id: 'this-is-the-server-id'
575 | });
576 | };
577 |
578 | var customers = new Customers();
579 | var customer = new Customer();
580 |
581 | customers.add(customer);
582 |
583 | var deleteCustomer = function deleteCustomer(model) {
584 | customer.destroy({
585 | success: function() {
586 | assert.equal(customers.length, 0, 'No items in the customers collection');
587 | checkDirtyStore();
588 | },
589 | error: done
590 | });
591 | };
592 |
593 | var checkDirtyStore = function checkDirtyStore() {
594 | var options = {
595 | key: 'this-is-the-server-id',
596 | storeName: 'customers',
597 | db: databaseId
598 | };
599 | getItem(options, function (err, result) {
600 | if (err) return done(err);
601 | assert.isUndefined(result, 'Customer should be deleted');
602 |
603 | getItem({ storeName: 'dirty', db: 'dirtystore' }, function (err, result) {
604 | if (err) return done(err);
605 | assert.isUndefined(result, 'Dirty store should be empty');
606 |
607 | getItem({ storeName: 'destroyed', db: 'dirtystore' }, function (err, result) {
608 | if (err) return done(err);
609 | assert.isUndefined(result, 'Destroyed store should be empty');
610 | done();
611 | });
612 | });
613 | });
614 | };
615 |
616 | customer.save({
617 | firstname: 'Tom',
618 | lastname: 'Smith',
619 | company: 'ACME',
620 |
621 | vat: '01234567890',
622 | iban: 'IT011C010000000000000012234'
623 | }, {
624 | success: deleteCustomer,
625 | error: done
626 | });
627 | });
628 |
629 | it('Should read a collection of customers', function (done) {
630 | Backbone.onlineSync = function (method, model, options) {
631 | var data = [
632 | {
633 | _id: 'this-is-the-server-id-A',
634 | firstname: 'Tom',
635 | lastname: 'Smith',
636 | company: 'ACME'
637 | },
638 | {
639 | _id: 'this-is-the-server-id-B',
640 | firstname: 'John',
641 | lastname: 'Johnson',
642 | company: 'ACME'
643 | }
644 | ];
645 | options.success(data);
646 | };
647 |
648 | var checkCustomerB = function checkCustomerB() {
649 | var options = {
650 | key: 'this-is-the-server-id-B',
651 | storeName: 'customers',
652 | db: databaseId
653 | };
654 | getItem(options, function (err, result) {
655 | if (err) return done(err);
656 | assert.equal(result.firstname, 'John');
657 | assert.equal(result.lastname, 'Johnson');
658 | done();
659 | });
660 | };
661 |
662 | var checkCustomerA = function checkCustomerA() {
663 | var options = {
664 | key: 'this-is-the-server-id-A',
665 | storeName: 'customers',
666 | db: databaseId
667 | };
668 | getItem(options, function (err, result) {
669 | if (err) return done(err);
670 | assert.equal(result.firstname, 'Tom');
671 | assert.equal(result.lastname, 'Smith');
672 | checkCustomerB();
673 | });
674 | };
675 |
676 | var customers = new Customers();
677 | var options = {
678 | success: checkCustomerA,
679 | error: done
680 | };
681 |
682 | customers.fetch(options);
683 | });
684 |
685 | it('Should read a single customer', function (done) {
686 | Backbone.onlineSync = function (method, model, options) {
687 | var data = {
688 | _id: 'this-is-the-server-id',
689 | firstname: 'Tom',
690 | lastname: 'Smith',
691 | company: 'ACME'
692 | };
693 | options.success(data);
694 | };
695 |
696 | var customerA = new Customer({
697 | '_id': 'this-is-the-server-id'
698 | });
699 |
700 | customerA.fetch({
701 | success: function() {
702 | assert.equal(customerA.get('firstname'), 'Tom');
703 | assert.equal(customerA.get('lastname'), 'Smith');
704 | assert.equal(customerA.get('company'), 'ACME');
705 | done();
706 | },
707 | error: done
708 | });
709 | });
710 |
711 | it('Should sync dirty data', function (done) {
712 |
713 | var serverCustomerA = {
714 | _id: 'this-is-the-server-A',
715 | firstname: 'Tom',
716 | lastname: 'Smith',
717 | company: 'ACME'
718 | };
719 | var serverCustomerB = {
720 | _id: 'this-is-the-server-B',
721 | firstname: 'John',
722 | lastname: 'Johnson',
723 | company: 'ACME'
724 | };
725 |
726 | var serverDB = {
727 | byId: {
728 | 'this-is-the-server-A': serverCustomerA,
729 | 'this-is-the-server-B': serverCustomerB
730 | },
731 | models: [serverCustomerA, serverCustomerB]
732 | };
733 |
734 | Backbone.onlineSync = function (method, model, options) {
735 | if (!Backbone.DualStorage.isOnline) {
736 | var fakeResponse = {
737 | status: 502,
738 | response: 'Fake bad gateway'
739 | };
740 | return options.error(fakeResponse);
741 | }
742 |
743 | switch (method) {
744 | case 'read':
745 | return options.success(serverDB.models);
746 | case 'update':
747 | var response = _.extend(serverDB.byId[model.id], model.attributes);
748 | return options.success(response);
749 | case 'delete':
750 | delete serverDB.byId[model.id];
751 | serverDB.models = [];
752 | for (var aModelId in serverDB.byId) {
753 | var aModel = serverDB.byId[aModelId];
754 | serverDB.models.push(aModel);
755 | }
756 | return options.success();
757 | };
758 | };
759 |
760 | var customers = new Customers();
761 |
762 | var syncOnline = function syncOnline() {
763 | // Emulate online
764 | Backbone.DualStorage.isOnline = true;
765 | customers.syncDirtyAndDestroyed(function (err, response) {
766 | if (err) return done(err);
767 | var cA = serverDB.byId['this-is-the-server-A'];
768 | assert.equal(cA.firstname, 'Tom');
769 | assert.equal(cA.lastname, 'Smith');
770 | assert.equal(cA.company, 'New Co.');
771 | var cB = serverDB.byId['this-is-the-server-B'];
772 | assert.isUndefined(cB);
773 | done();
774 | });
775 | };
776 |
777 | var readOnline = function readOnline() {
778 | // Emulate online
779 | Backbone.DualStorage.isOnline = true;
780 |
781 | var newCustomers = new Customers();
782 | newCustomers.fetch({
783 | success: function () {
784 | assert.equal(newCustomers.get('this-is-the-server-A').get('company'), 'New Co.', 'Should read dirty data');
785 | syncOnline();
786 | },
787 | error: done
788 | });
789 | };
790 |
791 | var writeDirty = function writeDirty() {
792 | var customerA = customers.get('this-is-the-server-A');
793 | var customerB = customers.get('this-is-the-server-B');
794 |
795 | assert.isNotNull(customerA);
796 | assert.equal(customerA.get('firstname'), 'Tom');
797 | assert.isNotNull(customerB);
798 |
799 | // Emulate offline
800 | Backbone.DualStorage.isOnline = false;
801 |
802 | // Edit customer A
803 | customerA.save({
804 | 'company': 'New Co.'
805 | }, {
806 | success: function (model) {
807 | assert.equal(model.get('company'), 'New Co.');
808 |
809 | var options = {
810 | db: databaseId,
811 | storeName: 'customers',
812 | key: 'this-is-the-server-A'
813 | };
814 | getItem(options, function (err, result) {
815 | if (err) return done(err);
816 | assert.equal(result.company, 'New Co.');
817 | // Delete customer B
818 | customerB.destroy({
819 | success: readOnline,
820 | error: done
821 | });
822 | });
823 | },
824 | error: done
825 | });
826 | };
827 |
828 | customers.fetch({
829 | success: writeDirty,
830 | error: done
831 | });
832 | });
833 |
834 | });
835 |
--------------------------------------------------------------------------------
/backbone.idbdualstorage.js:
--------------------------------------------------------------------------------
1 | ;(function (root, factory) {
2 |
3 | if (typeof define === 'function' && define.amd) {
4 | define(['backbone', 'underscore', 'backbone-indexeddb', 'idb'], function (Backbone, _, indexedDbSync, IDB) {
5 | var obj = factory(root, Backbone, _, indexedDbSync, IDB);
6 | root.Backbone.sync = obj.dualSync;
7 | return obj;
8 | });
9 | }
10 | else if (typeof exports !== 'undefined') {
11 | var Backbone = require('backbone');
12 | var _ = require('underscore');
13 | var indexedDbSync = require('backbone-indexeddb').sync;
14 | var IDB = require('idb');
15 | module.exports = factory(root, Backbone, _, indexedDbSync, IDB);
16 | }
17 | else {
18 | var obj = factory(root, root.Backbone, root._, root.Backbone.sync, root.IDB);
19 | root.Backbone.sync = obj.dualSync;
20 | root.DirtyStore = obj.DirtyStore;
21 | }
22 |
23 | }(this, function (root, Backbone, _, indexedDbSync, IDB) {
24 |
25 |
26 | // Generate four random hex digits.
27 | function S4() {
28 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
29 | }
30 | // Generate a pseudo-GUID by concatenating random hexadecimal.
31 | function guid() {
32 | return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
33 | }
34 |
35 |
36 | var instance;
37 |
38 | var DirtyStore = function DirtyStore() {};
39 |
40 | DirtyStore.getInstance = function getInstance(done) {
41 | /* // Singleton
42 | if (!instance) {
43 | var instance = new DirtyStore();
44 | instance.init(function (err) {
45 | if (err) return done(err);
46 | return done(null, instance);
47 | });
48 | }
49 | else {
50 | done(null, instance);
51 | }
52 | */
53 |
54 | var newInstance = new DirtyStore();
55 | newInstance.init(function (err) {
56 | if (err) return done(err);
57 | return done(null, newInstance);
58 | });
59 | };
60 |
61 | DirtyStore.prototype.init = function init(done) {
62 | var self = this;
63 |
64 | var options = {
65 | name: 'dirtystore',
66 | version: 1
67 | };
68 |
69 | var db = self.db = new IDB(options);
70 | db.onConnect = function() {
71 | done();
72 | };
73 | db.onError = function (err) {
74 | done(err);
75 | };
76 | db.onUpgrade = function(db, oldVersion, newVersion) {
77 | // Version 1
78 | if (oldVersion < 1 && 1 <= newVersion) {
79 | var dirtyStore = db.createObjectStore('dirty', { keyPath: 'id', autoIncrement: false });
80 | dirtyStore.createIndex('modelIdAndStoreNameIndex', ['modelId', 'storeName'], { unique: true });
81 | dirtyStore.createIndex('storeNameIndex', 'storeName', { unique: false });
82 |
83 | var destroyedStore = db.createObjectStore('destroyed', { keyPath: 'id', autoIncrement: false });
84 | destroyedStore.createIndex('modelIdAndStoreNameIndex', ['modelId', 'storeName'], { unique: true });
85 | destroyedStore.createIndex('storeNameIndex', 'storeName', { unique: false });
86 | }
87 | };
88 | };
89 |
90 | DirtyStore.prototype.addDirty = function addDirty(model, done) {
91 | var self = this;
92 |
93 | self.findDirty(model, function (err, result) {
94 | if (err) return done(err);
95 | if (result) return done(null, result.id);
96 | var data = {
97 | id: self.getNewId(),
98 | modelId: model.id,
99 | storeName: _.result(model, 'storeName')
100 | };
101 | self.db.add('dirty', data, done);
102 | });
103 |
104 | return self;
105 | };
106 |
107 | DirtyStore.prototype.addDestroyed = function addDestroyed(model, done) {
108 | var self = this;
109 |
110 | self.findDestroyed(model, function (err, result) {
111 | if (err) return done(err);
112 | if (result) return done(null, result.id);
113 | var data = {
114 | id: self.getNewId(),
115 | modelId: model.id,
116 | storeName: _.result(model, 'storeName')
117 | };
118 | self.db.add('destroyed', data, done);
119 | });
120 |
121 | return self;
122 | };
123 |
124 | DirtyStore.prototype.clear = function clear(done) {
125 | var self = this;
126 | self.db.clear('dirty', function (err) {
127 | if (err) return done(err);
128 | self.db.clear('destroyed', function (err) {
129 | if (err) return done(err);
130 | done();
131 | });
132 | });
133 | };
134 |
135 | DirtyStore.prototype.hasDirtyOrDestroyed = function hasDirtyOrDestroyed(done) {
136 | var self = this;
137 |
138 | self.db.count('dirty', function (err, count) {
139 | if (err) return done(err);
140 | if (count > 0) return done(null, true);
141 |
142 | self.db.count('destroyed', function (err, count) {
143 | if (err) return done(err);
144 | if (count > 0) return done(null, true);
145 | return done(null, false);
146 | });
147 | });
148 | };
149 |
150 | DirtyStore.prototype.findDirty = function findDirty(model, done) {
151 | var self = this;
152 | var store = _.result(model, 'storeName');
153 |
154 | if (!model.id)
155 | return done();
156 |
157 | var conditions = {
158 | index: 'modelIdAndStoreNameIndex',
159 | keyRange: self.db.makeKeyRange({
160 | only: [model.id, store]
161 | })
162 | };
163 |
164 | self.db.find('dirty', conditions, function (err, result) {
165 | if (err) return done(err);
166 | if (result.length > 0)
167 | done(null, result[0]);
168 | else
169 | done();
170 | });
171 | };
172 |
173 | DirtyStore.prototype.findAllDirty = function findAllDirty(store, done) {
174 | var self = this;
175 |
176 | var conditions = {
177 | index: 'storeNameIndex',
178 | keyRange: self.db.makeKeyRange({
179 | only: store
180 | }),
181 | };
182 |
183 | self.db.find('dirty', conditions, function (err, results) {
184 | if (err) return done(err);
185 | done(null, results);
186 | });
187 | };
188 |
189 | DirtyStore.prototype.findDestroyed = function findDestroyed(model, done) {
190 | var self = this;
191 | var store = _.result(model, 'storeName');
192 |
193 | if (!model.id)
194 | return done();
195 |
196 | var conditions = {
197 | index: 'modelIdAndStoreNameIndex',
198 | keyRange: self.db.makeKeyRange({
199 | only: [model.id, store]
200 | })
201 | };
202 |
203 | self.db.find('destroyed', conditions, function (err, result) {
204 | if (err) return done(err);
205 | if (result.length > 0)
206 | done(null, result[0]);
207 | else
208 | done();
209 | });
210 | };
211 |
212 | DirtyStore.prototype.findAllDestroyed = function findAllDestroyed(store, done) {
213 | var self = this;
214 |
215 | var conditions = {
216 | index: 'storeNameIndex',
217 | keyRange: self.db.makeKeyRange({
218 | only: store
219 | }),
220 | };
221 |
222 | self.db.find('destroyed', conditions, function (err, results) {
223 | if (err) return done(err);
224 | done(null, results);
225 | });
226 | };
227 |
228 | DirtyStore.prototype.removeDirty = function removeDirty(model, done) {
229 | var self = this;
230 |
231 | var removed = function removed(err) {
232 | if (err) return done(err);
233 | done();
234 | };
235 |
236 | self.findDirty(model, function (err, result) {
237 | if (err) return done(err);
238 | if (!result) return done(null, result);
239 | self.db.delete('dirty', result.id, removed);
240 | });
241 | };
242 |
243 | DirtyStore.prototype.removeDestroyed = function removeDestroyed(model, done) {
244 | var self = this;
245 |
246 | var removed = function removed(err) {
247 | if (err) return done(err);
248 | done();
249 | };
250 |
251 | self.findDestroyed(model, function (err, result) {
252 | if (err) return done(err);
253 | if (!result) return done(null, result);
254 | self.db.delete('destroyed', result.id, removed);
255 | });
256 | };
257 |
258 | DirtyStore.prototype.reset = function reset(store, done) {
259 | var self = this;
260 | self.resetDirty(store, function (err) {
261 | if (err) return done(err);
262 | self.resetDestroyed(store, function (err) {
263 | if (err) return done(err);
264 | done();
265 | });
266 | });
267 | };
268 |
269 | DirtyStore.prototype.resetDirty = function resetDirty(store, done) {
270 | var self = this;
271 | var conditions = {
272 | index: 'storeNameIndex',
273 | keyRange: self.db.makeKeyRange({
274 | only: store
275 | }),
276 | };
277 | self.db.deleteAll('dirty', conditions, function (err) {
278 | if (err) return done(err);
279 | done();
280 | });
281 | };
282 |
283 | DirtyStore.prototype.resetDestroyed = function resetDestroyed(store, done) {
284 | var self = this;
285 | var conditions = {
286 | index: 'storeNameIndex',
287 | keyRange: self.db.makeKeyRange({
288 | only: store
289 | }),
290 | };
291 | self.db.deleteAll('destroyed', conditions, function (err) {
292 | if (err) return done(err);
293 | done();
294 | });
295 | };
296 |
297 | DirtyStore.prototype.close = function close() {
298 | this.db.close();
299 | };
300 |
301 | DirtyStore.prototype.getNewId = function getNewId() {
302 | return guid();
303 | };
304 |
305 |
306 |
307 |
308 |
309 |
310 | Backbone.DualStorage = {
311 | persistent: false, // Use it if you need a persistent connection (not implemented yet)
312 | forceOffline: false, // change to true to emulate the offline mode
313 | offlineStatusCodes: [408, 502]
314 | };
315 |
316 | // Utility function
317 | var modelUpdatedWithResponse = function modelUpdatedWithResponse(model, response) {
318 | var modelClone;
319 | modelClone = new Backbone.Model;
320 | modelClone.idAttribute = model.idAttribute;
321 | modelClone.database = model.database;
322 | modelClone.storeName = model.storeName;
323 | modelClone.set(model.attributes);
324 | modelClone.set(model.parse(response));
325 | return modelClone;
326 | };
327 |
328 | var parseRemoteResponse = function parseRemoteResponse(object, response) {
329 | if (!(object && object.parseBeforeLocalSave)) {
330 | return response;
331 | }
332 | if (_.isFunction(object.parseBeforeLocalSave)) {
333 | return object.parseBeforeLocalSave(response);
334 | }
335 | };
336 |
337 | // async.js#eachSeries
338 | var eachSeries = function eachSeries(arr, iterator, callback) {
339 | callback = callback || function () {};
340 | if (!arr.length) {
341 | return callback();
342 | }
343 | var completed = 0;
344 | var iterate = function () {
345 | iterator(arr[completed], function (err) {
346 | if (err) {
347 | callback(err);
348 | callback = function () {};
349 | }
350 | else {
351 | completed += 1;
352 | if (completed >= arr.length) {
353 | callback();
354 | }
355 | else {
356 | iterate();
357 | }
358 | }
359 | });
360 | };
361 | iterate();
362 | };
363 |
364 |
365 |
366 | Backbone.Model.prototype.hasTempId = function() {
367 | return _.isString(this.id) && this.id.length === 36;
368 | };
369 |
370 | Backbone.Collection.prototype.syncDirty = function syncDirty(done) {
371 | var self = this;
372 |
373 | var response = {};
374 | var save = function save(aModel, next) {
375 | aModel.save(null, {
376 | success: function (resp) {
377 | response[aModel.id] = resp;
378 | return next();
379 | },
380 | error: function (err) {
381 | return next(err);
382 | }
383 | });
384 | };
385 |
386 | var getDirtyModelIds = function (store, callback) {
387 | DirtyStore.getInstance(function (err, store) {
388 | if (err) return callback(err);
389 | store.findAllDirty(storeName, function (err, dirtyModels) {
390 | if (err) return callback(err);
391 | var dirtyModelIds = [];
392 | _.forEach(dirtyModels, function (aDirtyModel) {
393 | dirtyModelIds.push(aDirtyModel.modelId);
394 | });
395 | callback(null, dirtyModelIds);
396 | });
397 | });
398 | };
399 |
400 | var storeName = _.result(self, 'storeName');
401 | getDirtyModelIds(storeName, function (err, dirtyModelIds) {
402 | if (err) return done(err);
403 | var arrayOfDirtyModels = self.filter(function (aModel) {
404 | return dirtyModelIds.indexOf(aModel.id) >= 0;
405 | });
406 | eachSeries(arrayOfDirtyModels, save, function (err) {
407 | if (err) return done(err);
408 | done(null, response);
409 | });
410 | });
411 | };
412 |
413 | Backbone.Collection.prototype.syncDestroyed = function syncDestroyed(done) {
414 | var self = this;
415 |
416 | var response = {};
417 | var destroy = function destroy(anId, next) {
418 | var aModel = new self.model();
419 | aModel.set(_.result(aModel, 'idAttribute'), anId);
420 | aModel.collection = self;
421 | aModel.destroy({
422 | success: function (resp) {
423 | response[aModel.id] = resp;
424 | return next();
425 | },
426 | error: function (err) {
427 | return next(err);
428 | }
429 | });
430 | };
431 |
432 | var getDestroyedModelIds = function (store, callback) {
433 | DirtyStore.getInstance(function (err, store) {
434 | if (err) return callback(err);
435 | store.findAllDestroyed(storeName, function (err, destroyedModels) {
436 | if (err) return callback(err);
437 | var destroyedModelIds = [];
438 | _.forEach(destroyedModels, function (aDestroyedModel) {
439 | destroyedModelIds.push(aDestroyedModel.modelId);
440 | });
441 | callback(null, destroyedModelIds);
442 | });
443 | });
444 | };
445 |
446 | var storeName = _.result(self, 'storeName');
447 | getDestroyedModelIds(storeName, function (err, destroyedModelIds) {
448 | if (err) return done(err);
449 | eachSeries(destroyedModelIds, destroy, function (err) {
450 | if (err) return done(err);
451 | done(null, response);
452 | });
453 | });
454 | };
455 |
456 | Backbone.Collection.prototype.syncDirtyAndDestroyed = function syncDirtyAndDestroyed(done) {
457 | var self = this;
458 | self.syncDirty(function (err, dirtyResponse) {
459 | if (err) return done(err);
460 | self.syncDestroyed(function (err, destroyedResponse) {
461 | if (err) return done(err);
462 | var response = {
463 | dirty: dirtyResponse,
464 | destroyed: destroyedResponse
465 | };
466 | done(null, response);
467 | });
468 | });
469 | };
470 |
471 |
472 |
473 | var onlineSync = function onlineSync(method, model, options) {
474 | if (Backbone.DualStorage.forceOffline) {
475 | var fakeResponse = {
476 | status: 502,
477 | response: 'Fake bad gateway'
478 | };
479 | return options.error(fakeResponse);
480 | }
481 |
482 | var _ajaxSync = Backbone.ajaxSync;
483 | return _ajaxSync(method, model, options);
484 | };
485 |
486 | var localSync = function localSync(method, model, options) {
487 | var _indexedDbSync = Backbone.indexedDbSync;
488 | var success = options.success;
489 | var error = options.error;
490 |
491 | var onReady = function onReady(err, store) {
492 | if (err) return error(err);
493 |
494 | var responseHandler = function responseHandler(err, result) {
495 | // Chiudo la connessione al DB
496 | store.close();
497 | if (err) return error(err);
498 | success(result);
499 | };
500 |
501 | switch (method) {
502 | case 'read':
503 | options.success = function(resp) {
504 | return responseHandler(null, resp);
505 | };
506 | options.error = function(err) {
507 | return responseHandler(err);
508 | };
509 | _indexedDbSync(method, model, options);
510 | break;
511 | case 'create':
512 | if (options.add && !options.merge) {
513 | store.findDirty(model, responseHandler);
514 | }
515 | else {
516 | options.success = function (resp) {
517 | if (options.dirty) {
518 | var updatedModel = modelUpdatedWithResponse(model, resp);
519 | store.addDirty(updatedModel, function (err) {
520 | return responseHandler(err, updatedModel.attributes);
521 | });
522 | }
523 | else {
524 | return responseHandler(null, model.attributes);
525 | }
526 | };
527 | options.error = function (err) {
528 | responseHandler(err);
529 | };
530 | _indexedDbSync(method, model, options);
531 | }
532 | break;
533 | case 'update':
534 | options.success = function (resp) {
535 | if (options.dirty) {
536 | store.addDirty(model, function (err) {
537 | responseHandler(err, resp);
538 | });
539 | }
540 | else {
541 | store.removeDirty(model, function (err) {
542 | responseHandler(err, resp);
543 | });
544 | }
545 | };
546 | _indexedDbSync(method, model, options);
547 | break;
548 | case 'delete':
549 | options.success = function (resp) {
550 | if (options.dirty) {
551 | if (model.hasTempId()) {
552 | return store.removeDirty(model, responseHandler);
553 | }
554 | else {
555 | return store.addDestroyed(model, responseHandler);
556 | }
557 | }
558 | else {
559 | if (model.hasTempId()) {
560 | return store.removeDirty(model, responseHandler);
561 | }
562 | else {
563 | return store.removeDestroyed(model, responseHandler);
564 | }
565 | }
566 | };
567 | _indexedDbSync(method, model, options);
568 | break;
569 | case 'closeall':
570 | _indexedDbSync(method, model, options);
571 | responseHandler();
572 | break;
573 | case 'hasDirtyOrDestroyed':
574 | store.hasDirtyOrDestroyed(responseHandler);
575 | break;
576 | case 'reset':
577 | if (model instanceof Backbone.Collection) {
578 | var collection = model;
579 | var storeName = _.result(collection.model.prototype, 'idAttribute');
580 | store.reset(storeName, responseHandler);
581 | }
582 | else {
583 | var storeName = _.result(model.prototype, 'idAttribute');
584 | store.reset(storeName, responseHandler);
585 | }
586 | break;
587 | }
588 | };
589 |
590 | DirtyStore.getInstance(onReady);
591 | };
592 |
593 | var dualSync = function dualSync(method, model, options) {
594 | var _localSync = Backbone.localSync;
595 | var _onlineSync = Backbone.onlineSync;
596 | var success = options.success;
597 | var error = options.error;
598 |
599 | if (_.result(model, 'local')) {
600 | return _localSync(method, model, options);
601 | }
602 |
603 | if (_.result(model, 'remote')) {
604 | return _onlineSync(method, model, options);
605 | }
606 |
607 | var relayErrorCallback = function relayErrorCallback(response) {
608 | var _ref;
609 | var offline;
610 | var offlineStatusCodes = Backbone.DualStorage.offlineStatusCodes;
611 | offline = response.status === 0 || (_ref = response.status, [].indexOf.call(offlineStatusCodes, _ref) >= 0);
612 | if (offline) {
613 | options.dirty = true;
614 | options.success = function(resp) {
615 | return success(resp);
616 | };
617 | options.error = function(err) {
618 | return error(err);
619 | };
620 | _localSync(method, model, options);
621 | }
622 | else {
623 | return error(response);
624 | }
625 | };
626 |
627 | switch (method) {
628 | case 'read':
629 | options.success = function(hasDirty) {
630 | if (hasDirty) {
631 | options.dirty = true;
632 | options.success = function(resp) {
633 | return success(resp);
634 | };
635 | options.error = function(err) {
636 | return error(err);
637 | };
638 | _localSync(method, model, options)
639 | }
640 | else {
641 | options.success = function(resp, status, xhr) {
642 | var responseModel;
643 | resp = parseRemoteResponse(model, resp);
644 | if (model instanceof Backbone.Collection) {
645 | var collection = model;
646 | var idAttribute = collection.model.prototype.idAttribute;
647 |
648 | var updateLocalDB = function updateLocalDB() {
649 | var update = function update(modelAttributes, next) {
650 | var aModel = collection.get(modelAttributes[idAttribute]);
651 | if (aModel) {
652 | responseModel = modelUpdatedWithResponse(aModel, modelAttributes);
653 | }
654 | else {
655 | responseModel = new collection.model(modelAttributes);
656 | }
657 | options.success = function() {
658 | next();
659 | };
660 | options.error = function(err) {
661 | next(err)
662 | };
663 | _localSync('update', responseModel, options);
664 | };
665 | eachSeries(resp, update, function (err) {
666 | if (err) return error(err);
667 | return success(resp, status, xhr);
668 | });
669 | };
670 |
671 | if (!options.add) {
672 | options.success = function() {
673 | updateLocalDB();
674 | };
675 | options.error = function(err) {
676 | return error(err);
677 | };
678 | _localSync('reset', collection, options);
679 | }
680 | else {
681 | updateLocalDB();
682 | }
683 | }
684 | else {
685 | responseModel = modelUpdatedWithResponse(model, resp);
686 | options.success = function(updatedResp) {
687 | return success(updatedResp);
688 | };
689 | options.error = function(err) {
690 | return error(err);
691 | };
692 | _localSync('update', responseModel, options);
693 | }
694 | };
695 | options.error = function(resp) {
696 | return relayErrorCallback(resp);
697 | };
698 | return _onlineSync(method, model, options);
699 | }
700 | };
701 | options.error = function(err) {
702 | return error(err);
703 | };
704 | _localSync('hasDirtyOrDestroyed', model, options);
705 | break;
706 |
707 | case 'create':
708 | options.success = function(resp, status, xhr) {
709 | var updatedModel;
710 | updatedModel = modelUpdatedWithResponse(model, resp);
711 | options.success = function(resp) {
712 | return success(resp);
713 | };
714 | options.error = function(err) {
715 | return error(err);
716 | };
717 | _localSync(method, updatedModel, options);
718 | };
719 | options.error = function(resp) {
720 | return relayErrorCallback(resp);
721 | };
722 | return _onlineSync(method, model, options);
723 | break;
724 |
725 | case 'clear':
726 | _localSync(method, model, options);
727 | break;
728 |
729 | case 'closeall':
730 | _localSync(method, model, options);
731 | break;
732 |
733 | case 'update':
734 | if (model.hasTempId()) {
735 | var temporaryId = model.id;
736 | options.success = function(resp, status, xhr) {
737 | var updatedModel;
738 | updatedModel = modelUpdatedWithResponse(model, resp);
739 | model.set(model.idAttribute, temporaryId, {
740 | silent: true
741 | });
742 | options.success = function() {
743 | options.success = function() {
744 | return success(resp, status, xhr);
745 | };
746 | options.error = function(err) {
747 | return error(err);
748 | };
749 | _localSync('create', updatedModel, options);
750 | };
751 | options.error = function(resp, status, xhr) {
752 | return error(err);
753 | };
754 | _localSync('delete', model, options);
755 | };
756 | options.error = function(resp) {
757 | model.set(model.idAttribute, temporaryId, {
758 | silent: true
759 | });
760 | return relayErrorCallback(resp);
761 | };
762 | model.set(model.idAttribute, null, {
763 | silent: true
764 | });
765 | return _onlineSync('create', model, options);
766 | }
767 | else {
768 | options.success = function(resp, status, xhr) {
769 | var updatedModel;
770 | updatedModel = modelUpdatedWithResponse(model, resp);
771 | options.success = function(model) {
772 | return success(resp);
773 | };
774 | options.error = function(err) {
775 | return error(err);
776 | };
777 | _localSync(method, updatedModel, options);
778 | };
779 | options.error = function(resp) {
780 | return relayErrorCallback(resp);
781 | };
782 | return _onlineSync(method, model, options);
783 | }
784 | break;
785 |
786 | case 'delete':
787 | if (model.hasTempId()) {
788 | return _localSync(method, model, options);
789 | }
790 | else {
791 | options.success = function(resp, status, xhr) {
792 | options.success = function() {
793 | return success(resp);
794 | };
795 | options.error = function(err) {
796 | return error(err);
797 | };
798 | return _localSync(method, model, options);
799 | };
800 | options.error = function(resp) {
801 | return relayErrorCallback(resp);
802 | };
803 | return _onlineSync(method, model, options);
804 | }
805 | break;
806 | }
807 | };
808 |
809 | // backbone-indexeddb puts the original Backbone.sync into Backbone.ajaxSync,
810 | // this behaviour, could change in the near future, and I hope it does,
811 | // so I applied this workaround to be sure that all will works fine
812 | if (typeof Backbone.ajaxSync === 'undefined')
813 | Backbone.ajaxSync = Backbone.sync;
814 |
815 | Backbone.indexedDbSync = indexedDbSync;
816 | Backbone.onlineSync = onlineSync;
817 | Backbone.localSync = localSync;
818 | Backbone.dualSync = dualSync;
819 |
820 | return {
821 | dualSync: Backbone.dualSync,
822 | DirtyStore: DirtyStore
823 | };
824 | }));
825 |
826 |
--------------------------------------------------------------------------------