├── .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 | --------------------------------------------------------------------------------