├── .gitignore ├── .travis.yml ├── History.md ├── LICENSE ├── Readme.md ├── bower.json ├── component.json ├── dist ├── treo-promise.js ├── treo-websql.js └── treo.js ├── examples ├── es6-generators.js ├── find-in-plugin.js └── key-value-storage.js ├── lib ├── idb-index.js ├── idb-store.js ├── index.js └── schema.js ├── package.json ├── plugins ├── treo-promise │ └── index.js └── treo-websql │ ├── index.js │ └── indexeddb-shim.js └── test ├── fixtures └── npm-data.json ├── integration.js └── treo.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | ## 0.5.1 / 2015-11-03 2 | 3 | * fix IE/Edge InvalidAccessError [#37](https://github.com/treojs/treo/pull/37) [@unkillbob](https://github.com/unkillbob) 4 | * deps: IndexedDBShim@2.2.1, idb-request@3.0.0, promise@7.0.4 5 | * use component-type@1.1.0 to prevent size blow [#22](https://github.com/component/type/issues/22) 6 | 7 | ## 0.5.0 / 2015-04-01 8 | 9 | * add support for multi-field indexes [#24](https://github.com/treojs/treo/issues/24) 10 | * add support for web-worker environment 11 | * deps: idb-range@2.3.0 12 | * add LICENSE file 13 | 14 | ## 0.4.3 / 2015-03-09 15 | 16 | * add support for iOS 8.1.3 [#26](https://github.com/treojs/treo/pull/26) 17 | 18 | Thanks: [@mariusk](https://github.com/mariusk) 19 | 20 | ## 0.4.2 / 2015-02-13 21 | 22 | * fix the iOS issue on IndexedDBShim [#21](https://github.com/treojs/treo/pull/21) 23 | 24 | Thanks: [@capsula4](https://github.com/capsula4) 25 | 26 | ## 0.4.1 / 2015-01-17 27 | 28 | * use [idb-range](https://github.com/treojs/idb-range), and remove treo.range 29 | * handle `onversionchange` automatically and close db [#17](https://github.com/treojs/treo/issues/16) 30 | 31 | ## 0.4.0 / 2015-01-16 [PR](https://github.com/treojs/treo/pull/18) 32 | 33 | * pass treo as second argument to `db.use()` 34 | * add `schema.dropStore()` and `schema.dropIndex()` 35 | * fix iOS 8 & Safari 7.0.6, IE10 & IE11 support 36 | * `put` passes the key of the created/updated record to callback 37 | 38 | Thanks: [@unkillbob](https://github.com/unkillbob) for fixing websql polyfill. 39 | 40 | ## 0.3.0 / 2014-09-24 41 | 42 | * add npm's files option for smaller tarbal 43 | * use dist folder for bower and component 44 | * add plugins/treo-websql pass same tests as real IndexedDB 45 | * add plugins/treo-promise for better promises support 46 | * add es6-generators example 47 | * update Readme #11 48 | 49 | ## 0.2.0 / 2014-09-08 50 | 51 | * use browserify for build instead of component(1) 52 | * fix npm support 53 | * key || keyPath, increment || autoIncretement, multi || multiEntry 54 | * (fix #12) schema.addStore({ increment: true }) to support autoIncrement 55 | 56 | ## 0.1.0 / 2014-07-23 57 | 58 | * initial release. 59 | It used to call indexed, and now this name exists only in git history 60 | as a prove of evolutionary thinking about this problem. 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Aleksey Kulikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Treo 2 | 3 | [![](https://img.shields.io/npm/v/treo.svg)](https://npmjs.org/package/treo) 4 | [![](https://img.shields.io/travis/treojs/treo.svg)](https://travis-ci.org/treojs/treo) 5 | [![](http://img.shields.io/npm/dm/treo.svg)](https://npmjs.org/package/treo) 6 | 7 | Treo is a lightweight wrapper around [IndexedDB](http://www.w3.org/TR/IndexedDB/) to make browser storage more enjoyable to use. 8 | Think about it as jQuery for IndexedDB. It does not add new abstractions, but simplifies the API and increases code reliability. 9 | 10 | I spent a lot of time reading the official specification and understanding its nuances. 11 | With treo I want to save this time for other developers, and help to focus on real problems and making the web better, 12 | instead of fighting with the complex IndexedDB API and stumbling on simple tasks. 13 | 14 | IndexedDB is powerful technology with support of indexes, stores, transactions and cursors. 15 | It allows us to build any kind of client databases. And let's be clear, it's the only real option to store data in browser, 16 | because localStorage is [synchronous](https://hacks.mozilla.org/2012/03/there-is-no-simple-solution-for-local-storage/) and WebSQL is deprecated. 17 | 18 | ## Main features 19 | 20 | * Simple API for powerful features like batch or indexes. 21 | * Command buffering, you can start read/write right away. 22 | * Small codebase without dependencies, ~370 LOC, 2.5Kb gziped. 23 | * Powerful DSL to manage database schema and versions. 24 | * Plugins for promises support and websql polyfill. 25 | * Better error handling through error first node-style callbacks. 26 | * Handle `versionchage` event automatically, to safely close and reopen database connection 27 | * Exposed access to low-level IndexedDB methods to cover edge cases. 28 | * Easy to extend and create plugins. 29 | 30 | ## Example 31 | 32 | Let's rewrite the official [w3c example](http://www.w3.org/TR/IndexedDB/#introduction) with treo: 33 | 34 | ```js 35 | var treo = require('treo'); // or window.treo 36 | 37 | // define db schema 38 | var schema = treo.schema() 39 | .version(1) 40 | .addStore('books', { key: 'isbn' }) 41 | .addIndex('byTitle', 'title', { unique: true }) 42 | .addIndex('byAuthor', 'author') 43 | .version(2) 44 | .getStore('books') 45 | .addIndex('byYear', 'year') 46 | .version(3) 47 | .addStore('magazines') 48 | .addIndex('byPublisher', 'publisher') 49 | .addIndex('byFrequency', 'frequency'); 50 | 51 | // open db 52 | var db = treo('library', schema); 53 | db.version; // 3 54 | 55 | // put some data in one transaction 56 | var books = db.store('books'); 57 | books.batch([ 58 | { isbn: 123456, title: 'Quarry Memories', author: 'Fred', year: 2012 }, 59 | { isbn: 234567, title: 'Water Buffaloes', author: 'Fred', year: 2012 }, 60 | { isbn: 345678, title: 'Bedrock Nights', author: 'Barney', year: 2013 }, 61 | ], function(err) { 62 | // Before this point, all actions were synchronous, and you don't need to wait 63 | // for db.open, initialize onupgradeneeded event, create readwrite transaction, 64 | // and handle all possible errors, blocks, aborts. 65 | // If any error happen on one of this steps, you get it as `err`. 66 | }); 67 | 68 | // get a single book by title using an index 69 | books.index('byTitle').get('Bedrock Nights', function(err, book) {}); 70 | 71 | // get all books filtered by author 72 | books.index('byAuthor').get('Fred', function(err, all) {}); // all.length == 2 73 | ``` 74 | 75 | For more examples check out [/examples](/examples): 76 | 77 | * [simple key value storage](/examples/key-value-storage.js) 78 | * [use ES6 generators and promises for nice async workflow](/examples/es6-generators.js) 79 | * [plugin example](/examples/find-in-plugin.js) 80 | 81 | ## Installation 82 | 83 | ``` 84 | $ npm install treo --save 85 | $ bower install treo 86 | $ component install treojs/treo 87 | ``` 88 | 89 | Standalone build available as [dist/treo.js](/dist/treo.js). 90 | 91 | ```html 92 | 93 | 94 | 95 | 100 | ``` 101 | 102 | ## Promises 103 | 104 | IndexedDB does not support ES6-Promises, but treo enables it with [treo-promise](https://github.com/treojs/treo/tree/master/plugins/treo-promise) plugin. 105 | 106 | ```js 107 | var promise = require('treo/plugins/treo-promise'); // or window.treoPromise 108 | var db = treo('library', schema) 109 | .use(promise()); 110 | 111 | var books = db.store('books'); 112 | 113 | Promise.all([ 114 | books.get('123456'), 115 | books.get('234567'), 116 | books.get('345678'), 117 | ]).then(function(records) { 118 | console.log(records); // records.length == 3 119 | 120 | books.count().then(function(count) { 121 | console.log(count); // total count of records 122 | }); 123 | }); 124 | ``` 125 | 126 | ## Legacy browsers 127 | 128 | IndexedDB is available only [in modern browsers](http://caniuse.com/#search=indexeddb), 129 | but we still need to support Safari <= 7 and legacy mobile browsers. 130 | Treo ships with [treo-websql](https://github.com/treojs/treo/tree/master/plugins/treo-websql) plugin, 131 | which enables fallback to WebSQL and fix all issues of [buggy IndexedDBShim](https://github.com/axemclion/IndexedDBShim/issues). 132 | In fact all treo's tests pass even in [phantomjs environment](https://travis-ci.org/treojs/treo). 133 | 134 | Usage: 135 | 136 | ```js 137 | var websql = require('treo/plugins/treo-websql'); // or window.treoWebsql 138 | var db = treo('library', schema) 139 | .use(websql()); 140 | ``` 141 | 142 | # API 143 | 144 | To initialize a new `db` instance, create a `schema` and pass it to main function. 145 | 146 | ```js 147 | // define schema with one storage 148 | var schema = treo.schema() 149 | .version(1) 150 | .addStore('storage'); 151 | 152 | // create db 153 | var db = treo('key-value-storage', schema); 154 | db.store('storage') 155 | .put('foo', 'value 1', fn); // connect, create db, put value 156 | ``` 157 | 158 | ## Schema 159 | 160 | Treo uses `treo.schema()` to setup internal objects like stores and indexes for right away access. 161 | Also, based on schema, treo generates `onupgradeneeded` callback. 162 | 163 | ### schema.version(version) 164 | 165 | Change current version. 166 | 167 | ### schema.addStore(name, opts) 168 | 169 | Declare store with `name`. 170 | Available options: 171 | * `key` - setup keyPath for easy work with objects [default false] 172 | * `increment` - generate incremental key automatically [default false] 173 | 174 | ### schema.addIndex(name, field, opts) 175 | 176 | Declare index with `name` to one specific `field`. 177 | It can be called after store declaration or use `schema.getStore(name)` to change current store. 178 | Available options: 179 | * `unique` - index is unique [default false] 180 | * `multi` - declare multi index for array type field [dafault false] 181 | 182 | ### schema.getStore(name) 183 | 184 | Change current store. 185 | 186 | ### schema.dropStore(name) 187 | 188 | Delete store by `name`. 189 | 190 | ### schema.dropIndex(name) 191 | 192 | Delete index by `name` from current store. 193 | 194 | ## DB 195 | 196 | It's an interface to manage db connections, create transactions and get access 197 | to stores, where real work happen. 198 | 199 | ### db.use(fn) 200 | 201 | Use plugin `fn(db, treo)`, it calls with `db` instance and `treo` object, 202 | so you don't need to require treo as dependencies. 203 | 204 | ### db.store(name) 205 | 206 | Get store by `name`. 207 | See [Store API](https://github.com/treojs/treo#store) for more information. 208 | 209 | ### db.close([fn]) 210 | 211 | Close db connection. Callback is optional when `db.status == 'open'`. 212 | 213 | ### db.drop(fn) 214 | 215 | Close connection and drop database. 216 | 217 | ### db.properties 218 | 219 | * version - db version 220 | * name - db name 221 | * status - connection status: close, opening, open 222 | * origin - original IDBDatabase instance 223 | 224 | ## Store 225 | 226 | Store is the primary storage mechanism for storing data. 227 | Think about it as table in SQL database. 228 | 229 | ### store.get(key, fn) 230 | 231 | Get value by `key`. 232 | 233 | ### store.put(key, val, fn) or store.put(obj, fn) 234 | 235 | Put `val` to `key`. Put means create or replace. 236 | If it's an object store with key property, you pass the whole object. 237 | `fn` callback returns error and key of new value. 238 | 239 | ```js 240 | var schema = treo.schema() 241 | .version(1) 242 | .addStore('books', { key: 'isbn' }); 243 | 244 | var db = treo('key-value-storage', schema); 245 | db.get('books').put({ isbn: 123456, title: 'Quarry Memories', author: 'Fred' }, fn); 246 | // key is isbn field and equal 123456 247 | ``` 248 | 249 | ### store.del(key, fn) 250 | 251 | Delete value by `key`. 252 | 253 | ### store.batch(opts, fn) 254 | 255 | Create/update/remove objects in one transaction. 256 | `opts` can be an object or an array (when key option is specified). 257 | 258 | ```js 259 | var db = treo('key-value-storage', schema); 260 | var storage = db.store('storage'); 261 | 262 | storage.put('key1', 'value 1', fn); 263 | storage.put('key2', 'value 2', fn); 264 | 265 | storage.batch({ 266 | key1: 'update value', 267 | key2: null, // delete value 268 | key3: 'new value', 269 | }, fn); 270 | ``` 271 | 272 | ### store.count(fn) 273 | 274 | Count records in store. 275 | 276 | ### store.all(fn) 277 | 278 | Get all records. 279 | 280 | ### store.clear(fn) 281 | 282 | Clear store. 283 | 284 | ### store.index(name) 285 | 286 | Get index by `name`. 287 | 288 | ## Index 289 | 290 | Index is a way to filter your data. 291 | 292 | ### index.get(key, fn) 293 | 294 | Get values by `key`. When index is unique it returns only one value. 295 | `key` can be string, [range](https://github.com/treojs/idb-range), or IDBKeyRange object. 296 | 297 | ```js 298 | books.index('byTitle').get('Bedrock Nights', fn); // get unique value 299 | books.index('byAuthor').get('Fred', fn); // get array of matching values 300 | books.index('byYear').get({ gte: 2012 }); 301 | books.index('byAuthor', IDBKeyRange.only('Barney')); 302 | ``` 303 | 304 | ### index.count(key, fn) 305 | 306 | Count records by `key`, similar to get, but returns number. 307 | 308 | ## Low Level Methods 309 | 310 | ### db.getInstance(cb) 311 | 312 | Connect to db and create defined stores. 313 | It's useful, when you need to handle edge cases related with using origin database object. 314 | 315 | ### db.transaction(type, stores, fn) 316 | 317 | Create new transaction to list of stores. 318 | Available types: `readonly` and `readwrite`. 319 | 320 | ### store.cursor(opts, fn), index.cursor(opts, fn) 321 | 322 | Create custom cursors, see [example](https://github.com/treojs/treo/blob/master/examples/find-in-plugin.js) and [article](https://hacks.mozilla.org/2014/06/breaking-the-borders-of-indexeddb/) for more detailed usage. 323 | 324 | ### treo.Treo, treo.Store, treo.Index 325 | 326 | Treo exposes core objects for plugins extension. 327 | 328 | ### treo.cmp(a, b) 329 | 330 | Compare 2 values using indexeddb's internal key compassion algorithm. 331 | 332 | ## License 333 | 334 | MIT 335 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "treo", 3 | "description": "Human interface for IndexedDB", 4 | "version": "0.5.1", 5 | "keywords": [ 6 | "IndexedDB", 7 | "treo", 8 | "offline" 9 | ], 10 | "ignore": [ 11 | "**/.*", 12 | "Makefile", 13 | "test/", 14 | "examples/", 15 | "lib/", 16 | "component.json", 17 | "package.json" 18 | ], 19 | "main": [ 20 | "dist/treo.js" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "treo", 3 | "description": "Human interface for IndexedDB", 4 | "repository": "treojs/treo", 5 | "main": "dist/treo.js", 6 | "version": "0.5.1", 7 | "license": "MIT", 8 | "keywords": [ 9 | "IndexedDB", 10 | "treo", 11 | "offline" 12 | ], 13 | "scripts": [ 14 | "dist/treo.js" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /dist/treo-promise.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.treoPromise = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 ? argumentCount : 0); 362 | return new Promise(function (resolve, reject) { 363 | args.push(function (err, res) { 364 | if (err) reject(err); 365 | else resolve(res); 366 | }) 367 | var res = fn.apply(self, args); 368 | if (res && 369 | ( 370 | typeof res === 'object' || 371 | typeof res === 'function' 372 | ) && 373 | typeof res.then === 'function' 374 | ) { 375 | resolve(res); 376 | } 377 | }) 378 | } 379 | } 380 | Promise.nodeify = function (fn) { 381 | return function () { 382 | var args = Array.prototype.slice.call(arguments); 383 | var callback = 384 | typeof args[args.length - 1] === 'function' ? args.pop() : null; 385 | var ctx = this; 386 | try { 387 | return fn.apply(this, arguments).nodeify(callback, ctx); 388 | } catch (ex) { 389 | if (callback === null || typeof callback == 'undefined') { 390 | return new Promise(function (resolve, reject) { 391 | reject(ex); 392 | }); 393 | } else { 394 | asap(function () { 395 | callback.call(ctx, ex); 396 | }) 397 | } 398 | } 399 | } 400 | } 401 | 402 | Promise.prototype.nodeify = function (callback, ctx) { 403 | if (typeof callback != 'function') return this; 404 | 405 | this.then(function (value) { 406 | asap(function () { 407 | callback.call(ctx, null, value); 408 | }); 409 | }, function (err) { 410 | asap(function () { 411 | callback.call(ctx, err); 412 | }); 413 | }); 414 | } 415 | 416 | },{"./core.js":2,"asap":8}],8:[function(require,module,exports){ 417 | "use strict"; 418 | 419 | // rawAsap provides everything we need except exception management. 420 | var rawAsap = require("./raw"); 421 | // RawTasks are recycled to reduce GC churn. 422 | var freeTasks = []; 423 | // We queue errors to ensure they are thrown in right order (FIFO). 424 | // Array-as-queue is good enough here, since we are just dealing with exceptions. 425 | var pendingErrors = []; 426 | var requestErrorThrow = rawAsap.makeRequestCallFromTimer(throwFirstError); 427 | 428 | function throwFirstError() { 429 | if (pendingErrors.length) { 430 | throw pendingErrors.shift(); 431 | } 432 | } 433 | 434 | /** 435 | * Calls a task as soon as possible after returning, in its own event, with priority 436 | * over other events like animation, reflow, and repaint. An error thrown from an 437 | * event will not interrupt, nor even substantially slow down the processing of 438 | * other events, but will be rather postponed to a lower priority event. 439 | * @param {{call}} task A callable object, typically a function that takes no 440 | * arguments. 441 | */ 442 | module.exports = asap; 443 | function asap(task) { 444 | var rawTask; 445 | if (freeTasks.length) { 446 | rawTask = freeTasks.pop(); 447 | } else { 448 | rawTask = new RawTask(); 449 | } 450 | rawTask.task = task; 451 | rawAsap(rawTask); 452 | } 453 | 454 | // We wrap tasks with recyclable task objects. A task object implements 455 | // `call`, just like a function. 456 | function RawTask() { 457 | this.task = null; 458 | } 459 | 460 | // The sole purpose of wrapping the task is to catch the exception and recycle 461 | // the task object after its single use. 462 | RawTask.prototype.call = function () { 463 | try { 464 | this.task.call(); 465 | } catch (error) { 466 | if (asap.onerror) { 467 | // This hook exists purely for testing purposes. 468 | // Its name will be periodically randomized to break any code that 469 | // depends on its existence. 470 | asap.onerror(error); 471 | } else { 472 | // In a web browser, exceptions are not fatal. However, to avoid 473 | // slowing down the queue of pending tasks, we rethrow the error in a 474 | // lower priority turn. 475 | pendingErrors.push(error); 476 | requestErrorThrow(); 477 | } 478 | } finally { 479 | this.task = null; 480 | freeTasks[freeTasks.length] = this; 481 | } 482 | }; 483 | 484 | },{"./raw":9}],9:[function(require,module,exports){ 485 | (function (global){ 486 | "use strict"; 487 | 488 | // Use the fastest means possible to execute a task in its own turn, with 489 | // priority over other events including IO, animation, reflow, and redraw 490 | // events in browsers. 491 | // 492 | // An exception thrown by a task will permanently interrupt the processing of 493 | // subsequent tasks. The higher level `asap` function ensures that if an 494 | // exception is thrown by a task, that the task queue will continue flushing as 495 | // soon as possible, but if you use `rawAsap` directly, you are responsible to 496 | // either ensure that no exceptions are thrown from your task, or to manually 497 | // call `rawAsap.requestFlush` if an exception is thrown. 498 | module.exports = rawAsap; 499 | function rawAsap(task) { 500 | if (!queue.length) { 501 | requestFlush(); 502 | flushing = true; 503 | } 504 | // Equivalent to push, but avoids a function call. 505 | queue[queue.length] = task; 506 | } 507 | 508 | var queue = []; 509 | // Once a flush has been requested, no further calls to `requestFlush` are 510 | // necessary until the next `flush` completes. 511 | var flushing = false; 512 | // `requestFlush` is an implementation-specific method that attempts to kick 513 | // off a `flush` event as quickly as possible. `flush` will attempt to exhaust 514 | // the event queue before yielding to the browser's own event loop. 515 | var requestFlush; 516 | // The position of the next task to execute in the task queue. This is 517 | // preserved between calls to `flush` so that it can be resumed if 518 | // a task throws an exception. 519 | var index = 0; 520 | // If a task schedules additional tasks recursively, the task queue can grow 521 | // unbounded. To prevent memory exhaustion, the task queue will periodically 522 | // truncate already-completed tasks. 523 | var capacity = 1024; 524 | 525 | // The flush function processes all tasks that have been scheduled with 526 | // `rawAsap` unless and until one of those tasks throws an exception. 527 | // If a task throws an exception, `flush` ensures that its state will remain 528 | // consistent and will resume where it left off when called again. 529 | // However, `flush` does not make any arrangements to be called again if an 530 | // exception is thrown. 531 | function flush() { 532 | while (index < queue.length) { 533 | var currentIndex = index; 534 | // Advance the index before calling the task. This ensures that we will 535 | // begin flushing on the next task the task throws an error. 536 | index = index + 1; 537 | queue[currentIndex].call(); 538 | // Prevent leaking memory for long chains of recursive calls to `asap`. 539 | // If we call `asap` within tasks scheduled by `asap`, the queue will 540 | // grow, but to avoid an O(n) walk for every task we execute, we don't 541 | // shift tasks off the queue after they have been executed. 542 | // Instead, we periodically shift 1024 tasks off the queue. 543 | if (index > capacity) { 544 | // Manually shift all values starting at the index back to the 545 | // beginning of the queue. 546 | for (var scan = 0, newLength = queue.length - index; scan < newLength; scan++) { 547 | queue[scan] = queue[scan + index]; 548 | } 549 | queue.length -= index; 550 | index = 0; 551 | } 552 | } 553 | queue.length = 0; 554 | index = 0; 555 | flushing = false; 556 | } 557 | 558 | // `requestFlush` is implemented using a strategy based on data collected from 559 | // every available SauceLabs Selenium web driver worker at time of writing. 560 | // https://docs.google.com/spreadsheets/d/1mG-5UYGup5qxGdEMWkhP6BWCz053NUb2E1QoUTU16uA/edit#gid=783724593 561 | 562 | // Safari 6 and 6.1 for desktop, iPad, and iPhone are the only browsers that 563 | // have WebKitMutationObserver but not un-prefixed MutationObserver. 564 | // Must use `global` instead of `window` to work in both frames and web 565 | // workers. `global` is a provision of Browserify, Mr, Mrs, or Mop. 566 | var BrowserMutationObserver = global.MutationObserver || global.WebKitMutationObserver; 567 | 568 | // MutationObservers are desirable because they have high priority and work 569 | // reliably everywhere they are implemented. 570 | // They are implemented in all modern browsers. 571 | // 572 | // - Android 4-4.3 573 | // - Chrome 26-34 574 | // - Firefox 14-29 575 | // - Internet Explorer 11 576 | // - iPad Safari 6-7.1 577 | // - iPhone Safari 7-7.1 578 | // - Safari 6-7 579 | if (typeof BrowserMutationObserver === "function") { 580 | requestFlush = makeRequestCallFromMutationObserver(flush); 581 | 582 | // MessageChannels are desirable because they give direct access to the HTML 583 | // task queue, are implemented in Internet Explorer 10, Safari 5.0-1, and Opera 584 | // 11-12, and in web workers in many engines. 585 | // Although message channels yield to any queued rendering and IO tasks, they 586 | // would be better than imposing the 4ms delay of timers. 587 | // However, they do not work reliably in Internet Explorer or Safari. 588 | 589 | // Internet Explorer 10 is the only browser that has setImmediate but does 590 | // not have MutationObservers. 591 | // Although setImmediate yields to the browser's renderer, it would be 592 | // preferrable to falling back to setTimeout since it does not have 593 | // the minimum 4ms penalty. 594 | // Unfortunately there appears to be a bug in Internet Explorer 10 Mobile (and 595 | // Desktop to a lesser extent) that renders both setImmediate and 596 | // MessageChannel useless for the purposes of ASAP. 597 | // https://github.com/kriskowal/q/issues/396 598 | 599 | // Timers are implemented universally. 600 | // We fall back to timers in workers in most engines, and in foreground 601 | // contexts in the following browsers. 602 | // However, note that even this simple case requires nuances to operate in a 603 | // broad spectrum of browsers. 604 | // 605 | // - Firefox 3-13 606 | // - Internet Explorer 6-9 607 | // - iPad Safari 4.3 608 | // - Lynx 2.8.7 609 | } else { 610 | requestFlush = makeRequestCallFromTimer(flush); 611 | } 612 | 613 | // `requestFlush` requests that the high priority event queue be flushed as 614 | // soon as possible. 615 | // This is useful to prevent an error thrown in a task from stalling the event 616 | // queue if the exception handled by Node.js’s 617 | // `process.on("uncaughtException")` or by a domain. 618 | rawAsap.requestFlush = requestFlush; 619 | 620 | // To request a high priority event, we induce a mutation observer by toggling 621 | // the text of a text node between "1" and "-1". 622 | function makeRequestCallFromMutationObserver(callback) { 623 | var toggle = 1; 624 | var observer = new BrowserMutationObserver(callback); 625 | var node = document.createTextNode(""); 626 | observer.observe(node, {characterData: true}); 627 | return function requestCall() { 628 | toggle = -toggle; 629 | node.data = toggle; 630 | }; 631 | } 632 | 633 | // The message channel technique was discovered by Malte Ubl and was the 634 | // original foundation for this library. 635 | // http://www.nonblocking.io/2011/06/windownexttick.html 636 | 637 | // Safari 6.0.5 (at least) intermittently fails to create message ports on a 638 | // page's first load. Thankfully, this version of Safari supports 639 | // MutationObservers, so we don't need to fall back in that case. 640 | 641 | // function makeRequestCallFromMessageChannel(callback) { 642 | // var channel = new MessageChannel(); 643 | // channel.port1.onmessage = callback; 644 | // return function requestCall() { 645 | // channel.port2.postMessage(0); 646 | // }; 647 | // } 648 | 649 | // For reasons explained above, we are also unable to use `setImmediate` 650 | // under any circumstances. 651 | // Even if we were, there is another bug in Internet Explorer 10. 652 | // It is not sufficient to assign `setImmediate` to `requestFlush` because 653 | // `setImmediate` must be called *by name* and therefore must be wrapped in a 654 | // closure. 655 | // Never forget. 656 | 657 | // function makeRequestCallFromSetImmediate(callback) { 658 | // return function requestCall() { 659 | // setImmediate(callback); 660 | // }; 661 | // } 662 | 663 | // Safari 6.0 has a problem where timers will get lost while the user is 664 | // scrolling. This problem does not impact ASAP because Safari 6.0 supports 665 | // mutation observers, so that implementation is used instead. 666 | // However, if we ever elect to use timers in Safari, the prevalent work-around 667 | // is to add a scroll event listener that calls for a flush. 668 | 669 | // `setTimeout` does not call the passed callback if the delay is less than 670 | // approximately 7 in web workers in Firefox 8 through 18, and sometimes not 671 | // even then. 672 | 673 | function makeRequestCallFromTimer(callback) { 674 | return function requestCall() { 675 | // We dispatch a timeout with a specified delay of 0 for engines that 676 | // can reliably accommodate that request. This will usually be snapped 677 | // to a 4 milisecond delay, but once we're flushing, there's no delay 678 | // between events. 679 | var timeoutHandle = setTimeout(handleTimer, 0); 680 | // However, since this timer gets frequently dropped in Firefox 681 | // workers, we enlist an interval handle that will try to fire 682 | // an event 20 times per second until it succeeds. 683 | var intervalHandle = setInterval(handleTimer, 50); 684 | 685 | function handleTimer() { 686 | // Whichever timer succeeds will cancel both timers and 687 | // execute the callback. 688 | clearTimeout(timeoutHandle); 689 | clearInterval(intervalHandle); 690 | callback(); 691 | } 692 | }; 693 | } 694 | 695 | // This is for `asap.js` only. 696 | // Its name will be periodically randomized to break any code that depends on 697 | // its existence. 698 | rawAsap.makeRequestCallFromTimer = makeRequestCallFromTimer; 699 | 700 | // ASAP was originally a nextTick shim included in Q. This was factored out 701 | // into this ASAP package. It was later adapted to RSVP which made further 702 | // amendments. These decisions, particularly to marginalize MessageChannel and 703 | // to capture the MutationObserver implementation in a closure, were integrated 704 | // back into ASAP proper. 705 | // https://github.com/tildeio/rsvp.js/blob/cddf7232546a9cf858524b75cde6f9edf72620a7/lib/rsvp/asap.js 706 | 707 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 708 | },{}],10:[function(require,module,exports){ 709 | var denodeify = require('promise').denodeify; 710 | 711 | /** 712 | * Expose `plugin()`. 713 | */ 714 | 715 | module.exports = plugin; 716 | 717 | /** 718 | * Methods for patch. 719 | */ 720 | 721 | var dbMethods = [ 722 | ['drop', 1], 723 | ['close', 1] 724 | ]; 725 | 726 | var storeMethods = [ 727 | ['put', 3], 728 | ['get', 2], 729 | ['del', 2], 730 | ['count', 1], 731 | ['clear', 1], 732 | ['batch', 2], 733 | ['all', 1], 734 | ]; 735 | 736 | var indexMethods = [ 737 | ['get', 2], 738 | ['count', 2], 739 | ]; 740 | 741 | /** 742 | * Denodeify each db's method and add promises support with 743 | * https://github.com/jakearchibald/es6-promise 744 | */ 745 | 746 | function plugin() { 747 | return function(db) { 748 | patch(db, dbMethods); 749 | 750 | Object.keys(db.stores).forEach(function(storeName) { 751 | var store = db.store(storeName); 752 | patch(store, storeMethods); 753 | 754 | Object.keys(store.indexes).forEach(function(indexName) { 755 | var index = store.index(indexName); 756 | patch(index, indexMethods); 757 | }); 758 | }); 759 | }; 760 | } 761 | 762 | /** 763 | * Patch `methods` from `object` with `denodeify`. 764 | * 765 | * @param {Object} object 766 | * @param {Array} methods 767 | */ 768 | 769 | function patch(object, methods) { 770 | methods.forEach(function(m) { 771 | object[m[0]] = denodeify(object[m[0]], m[1]); 772 | }); 773 | } 774 | 775 | },{"promise":1}]},{},[10])(10) 776 | }); -------------------------------------------------------------------------------- /dist/treo.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.treo = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= keys.length) return; 243 | var currentKey = keys[current]; 244 | var currentVal = vals[currentKey]; 245 | var req; 246 | 247 | if (currentVal === null) { 248 | req = store.delete(currentKey); 249 | } else if (keyPath) { 250 | if (!currentVal[keyPath]) currentVal[keyPath] = currentKey; 251 | req = store.put(currentVal); 252 | } else { 253 | req = store.put(currentVal, currentKey); 254 | } 255 | 256 | req.onerror = cb; 257 | req.onsuccess = next; 258 | current += 1; 259 | } 260 | }); 261 | }; 262 | 263 | /** 264 | * Get all. 265 | * 266 | * @param {Function} cb 267 | */ 268 | 269 | Store.prototype.all = function(cb) { 270 | var result = []; 271 | 272 | this.cursor({ iterator: iterator }, function(err) { 273 | err ? cb(err) : cb(null, result); 274 | }); 275 | 276 | function iterator(cursor) { 277 | result.push(cursor.value); 278 | cursor.continue(); 279 | } 280 | }; 281 | 282 | /** 283 | * Create read cursor for specific `range`, 284 | * and pass IDBCursor to `iterator` function. 285 | * https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor 286 | * 287 | * @param {Object} opts: 288 | * {IDBRange|Object} range - passes to .openCursor() 289 | * {Function} iterator - function to call with IDBCursor 290 | * {String} [index] - name of index to start cursor by index 291 | * @param {Function} cb - calls on end or error 292 | */ 293 | 294 | Store.prototype.cursor = function(opts, cb) { 295 | var name = this.name; 296 | this.db.transaction('readonly', [name], function(err, tr) { 297 | if (err) return cb(err); 298 | var store = opts.index 299 | ? tr.objectStore(name).index(opts.index) 300 | : tr.objectStore(name); 301 | var req = store.openCursor(parseRange(opts.range)); 302 | 303 | req.onerror = cb; 304 | req.onsuccess = function onsuccess(e) { 305 | var cursor = e.target.result; 306 | cursor ? opts.iterator(cursor) : cb(); 307 | }; 308 | }); 309 | }; 310 | 311 | },{"component-type":5,"idb-range":6}],3:[function(require,module,exports){ 312 | var type = require('component-type'); 313 | var Store = require('./idb-store'); 314 | var Index = require('./idb-index'); 315 | 316 | /** 317 | * Expose `Schema`. 318 | */ 319 | 320 | module.exports = Schema; 321 | 322 | /** 323 | * Initialize new `Schema`. 324 | */ 325 | 326 | function Schema() { 327 | if (!(this instanceof Schema)) return new Schema(); 328 | this._stores = {}; 329 | this._current = {}; 330 | this._versions = {}; 331 | } 332 | 333 | /** 334 | * Set new version. 335 | * 336 | * @param {Number} version 337 | * @return {Schema} 338 | */ 339 | 340 | Schema.prototype.version = function(version) { 341 | if (type(version) != 'number' || version < 1 || version < this.getVersion()) 342 | throw new TypeError('not valid version'); 343 | 344 | this._current = { version: version, store: null }; 345 | this._versions[version] = { 346 | stores: [], // db.createObjectStore 347 | dropStores: [], // db.deleteObjectStore 348 | indexes: [], // store.createIndex 349 | dropIndexes: [], // store.deleteIndex 350 | version: version // version 351 | }; 352 | 353 | return this; 354 | }; 355 | 356 | /** 357 | * Add store. 358 | * 359 | * @param {String} name 360 | * @param {Object} [opts] { key: false } 361 | * @return {Schema} 362 | */ 363 | 364 | Schema.prototype.addStore = function(name, opts) { 365 | if (type(name) != 'string') throw new TypeError('`name` is required'); 366 | if (this._stores[name]) throw new TypeError('store is already defined'); 367 | var store = new Store(name, opts || {}); 368 | this._stores[name] = store; 369 | this._versions[this.getVersion()].stores.push(store); 370 | this._current.store = store; 371 | return this; 372 | }; 373 | 374 | /** 375 | * Drop store. 376 | * 377 | * @param {String} name 378 | * @return {Schema} 379 | */ 380 | 381 | Schema.prototype.dropStore = function(name) { 382 | if (type(name) != 'string') throw new TypeError('`name` is required'); 383 | var store = this._stores[name]; 384 | if (!store) throw new TypeError('store is not defined'); 385 | delete this._stores[name]; 386 | this._versions[this.getVersion()].dropStores.push(store); 387 | return this; 388 | }; 389 | 390 | /** 391 | * Add index. 392 | * 393 | * @param {String} name 394 | * @param {String|Array} field 395 | * @param {Object} [opts] { unique: false, multi: false } 396 | * @return {Schema} 397 | */ 398 | 399 | Schema.prototype.addIndex = function(name, field, opts) { 400 | if (type(name) != 'string') throw new TypeError('`name` is required'); 401 | if (type(field) != 'string' && type(field) != 'array') throw new TypeError('`field` is required'); 402 | var store = this._current.store; 403 | if (store.indexes[name]) throw new TypeError('index is already defined'); 404 | var index = new Index(store, name, field, opts || {}); 405 | store.indexes[name] = index; 406 | this._versions[this.getVersion()].indexes.push(index); 407 | return this; 408 | }; 409 | 410 | /** 411 | * Drop index. 412 | * 413 | * @param {String} name 414 | * @return {Schema} 415 | */ 416 | 417 | Schema.prototype.dropIndex = function(name) { 418 | if (type(name) != 'string') throw new TypeError('`name` is required'); 419 | var index = this._current.store.indexes[name]; 420 | if (!index) throw new TypeError('index is not defined'); 421 | delete this._current.store.indexes[name]; 422 | this._versions[this.getVersion()].dropIndexes.push(index); 423 | return this; 424 | }; 425 | 426 | /** 427 | * Change current store. 428 | * 429 | * @param {String} name 430 | * @return {Schema} 431 | */ 432 | 433 | Schema.prototype.getStore = function(name) { 434 | if (type(name) != 'string') throw new TypeError('`name` is required'); 435 | if (!this._stores[name]) throw new TypeError('store is not defined'); 436 | this._current.store = this._stores[name]; 437 | return this; 438 | }; 439 | 440 | /** 441 | * Get version. 442 | * 443 | * @return {Number} 444 | */ 445 | 446 | Schema.prototype.getVersion = function() { 447 | return this._current.version; 448 | }; 449 | 450 | /** 451 | * Generate onupgradeneeded callback. 452 | * 453 | * @return {Function} 454 | */ 455 | 456 | Schema.prototype.callback = function() { 457 | var versions = Object.keys(this._versions) 458 | .map(function(v) { return this._versions[v] }, this) 459 | .sort(function(a, b) { return a.version - b.version }); 460 | 461 | return function onupgradeneeded(e) { 462 | var db = e.target.result; 463 | var tr = e.target.transaction; 464 | 465 | versions.forEach(function(versionSchema) { 466 | if (e.oldVersion >= versionSchema.version) return; 467 | 468 | versionSchema.stores.forEach(function(s) { 469 | var options = {}; 470 | 471 | // Only pass the options that are explicitly specified to createObjectStore() otherwise IE/Edge 472 | // can throw an InvalidAccessError - see https://msdn.microsoft.com/en-us/library/hh772493(v=vs.85).aspx 473 | if (typeof s.key !== 'undefined') options.keyPath = s.key; 474 | if (typeof s.increment !== 'undefined') options.autoIncrement = s.increment; 475 | 476 | db.createObjectStore(s.name, options); 477 | }); 478 | 479 | versionSchema.dropStores.forEach(function(s) { 480 | db.deleteObjectStore(s.name); 481 | }); 482 | 483 | versionSchema.indexes.forEach(function(i) { 484 | var store = tr.objectStore(i.store.name); 485 | store.createIndex(i.name, i.field, { 486 | unique: i.unique, 487 | multiEntry: i.multi 488 | }); 489 | }); 490 | 491 | versionSchema.dropIndexes.forEach(function(i) { 492 | var store = tr.objectStore(i.store.name); 493 | store.deleteIndex(i.name); 494 | }); 495 | }); 496 | }; 497 | }; 498 | 499 | },{"./idb-index":1,"./idb-store":2,"component-type":5}],4:[function(require,module,exports){ 500 | (function (global){ 501 | var type = require('component-type'); 502 | var Schema = require('./schema'); 503 | var Store = require('./idb-store'); 504 | var Index = require('./idb-index'); 505 | 506 | /** 507 | * Expose `Treo`. 508 | */ 509 | 510 | exports = module.exports = Treo; 511 | 512 | /** 513 | * Initialize new `Treo` instance. 514 | * 515 | * @param {String} name 516 | * @param {Schema} schema 517 | */ 518 | 519 | function Treo(name, schema) { 520 | if (!(this instanceof Treo)) return new Treo(name, schema); 521 | if (type(name) != 'string') throw new TypeError('`name` required'); 522 | if (!(schema instanceof Schema)) throw new TypeError('not valid schema'); 523 | 524 | this.name = name; 525 | this.status = 'close'; 526 | this.origin = null; 527 | this.stores = schema._stores; 528 | this.version = schema.getVersion(); 529 | this.onupgradeneeded = schema.callback(); 530 | 531 | // assign db property to each store 532 | Object.keys(this.stores).forEach(function(storeName) { 533 | this.stores[storeName].db = this; 534 | }, this); 535 | } 536 | 537 | /** 538 | * Expose core classes. 539 | */ 540 | 541 | exports.schema = Schema; 542 | exports.cmp = cmp; 543 | exports.Treo = Treo; 544 | exports.Schema = Schema; 545 | exports.Store = Store; 546 | exports.Index = Index; 547 | 548 | /** 549 | * Use plugin `fn`. 550 | * 551 | * @param {Function} fn 552 | * @return {Treo} 553 | */ 554 | 555 | Treo.prototype.use = function(fn) { 556 | fn(this, exports); 557 | return this; 558 | }; 559 | 560 | /** 561 | * Drop. 562 | * 563 | * @param {Function} cb 564 | */ 565 | 566 | Treo.prototype.drop = function(cb) { 567 | var name = this.name; 568 | this.close(function(err) { 569 | if (err) return cb(err); 570 | var req = indexedDB().deleteDatabase(name); 571 | req.onerror = cb; 572 | req.onsuccess = function onsuccess() { cb() }; 573 | }); 574 | }; 575 | 576 | /** 577 | * Close. 578 | * 579 | * @param {Function} cb 580 | */ 581 | 582 | Treo.prototype.close = function(cb) { 583 | if (this.status == 'close') return cb(); 584 | this.getInstance(function(err, db) { 585 | if (err) return cb(err); 586 | db.origin = null; 587 | db.status = 'close'; 588 | db.close(); 589 | cb(); 590 | }); 591 | }; 592 | 593 | /** 594 | * Get store by `name`. 595 | * 596 | * @param {String} name 597 | * @return {Store} 598 | */ 599 | 600 | Treo.prototype.store = function(name) { 601 | return this.stores[name]; 602 | }; 603 | 604 | /** 605 | * Get db instance. It starts opening transaction only once, 606 | * another requests will be scheduled to queue. 607 | * 608 | * @param {Function} cb 609 | */ 610 | 611 | Treo.prototype.getInstance = function(cb) { 612 | if (this.status == 'open') return cb(null, this.origin); 613 | if (this.status == 'opening') return this.queue.push(cb); 614 | 615 | this.status = 'opening'; 616 | this.queue = [cb]; // queue callbacks 617 | 618 | var that = this; 619 | var req = indexedDB().open(this.name, this.version); 620 | req.onupgradeneeded = this.onupgradeneeded; 621 | 622 | req.onerror = req.onblocked = function onerror(e) { 623 | that.status = 'error'; 624 | that.queue.forEach(function(cb) { cb(e) }); 625 | delete that.queue; 626 | }; 627 | 628 | req.onsuccess = function onsuccess(e) { 629 | that.origin = e.target.result; 630 | that.status = 'open'; 631 | that.origin.onversionchange = function onversionchange() { 632 | that.close(function() {}); 633 | }; 634 | that.queue.forEach(function(cb) { cb(null, that.origin) }); 635 | delete that.queue; 636 | }; 637 | }; 638 | 639 | /** 640 | * Create new transaction for selected `stores`. 641 | * 642 | * @param {String} type (readwrite|readonly) 643 | * @param {Array} stores - follow indexeddb semantic 644 | * @param {Function} cb 645 | */ 646 | 647 | Treo.prototype.transaction = function(type, stores, cb) { 648 | this.getInstance(function(err, db) { 649 | err ? cb(err) : cb(null, db.transaction(stores, type)); 650 | }); 651 | }; 652 | 653 | /** 654 | * Compare 2 values using IndexedDB comparision algotihm. 655 | * 656 | * @param {Mixed} value1 657 | * @param {Mixed} value2 658 | * @return {Number} -1|0|1 659 | */ 660 | 661 | function cmp() { 662 | return indexedDB().cmp.apply(indexedDB(), arguments); 663 | } 664 | 665 | /** 666 | * Dynamic link to `global.indexedDB` for polyfills support. 667 | * 668 | * @return {IDBDatabase} 669 | */ 670 | 671 | function indexedDB() { 672 | return global._indexedDB 673 | || global.indexedDB 674 | || global.msIndexedDB 675 | || global.mozIndexedDB 676 | || global.webkitIndexedDB; 677 | } 678 | 679 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 680 | },{"./idb-index":1,"./idb-store":2,"./schema":3,"component-type":5}],5:[function(require,module,exports){ 681 | /** 682 | * toString ref. 683 | */ 684 | 685 | var toString = Object.prototype.toString; 686 | 687 | /** 688 | * Return the type of `val`. 689 | * 690 | * @param {Mixed} val 691 | * @return {String} 692 | * @api public 693 | */ 694 | 695 | module.exports = function(val){ 696 | switch (toString.call(val)) { 697 | case '[object Date]': return 'date'; 698 | case '[object RegExp]': return 'regexp'; 699 | case '[object Arguments]': return 'arguments'; 700 | case '[object Array]': return 'array'; 701 | case '[object Error]': return 'error'; 702 | } 703 | 704 | if (val === null) return 'null'; 705 | if (val === undefined) return 'undefined'; 706 | if (val !== val) return 'nan'; 707 | if (val && val.nodeType === 1) return 'element'; 708 | 709 | val = val.valueOf 710 | ? val.valueOf() 711 | : Object.prototype.valueOf.apply(val) 712 | 713 | return typeof val; 714 | }; 715 | 716 | },{}],6:[function(require,module,exports){ 717 | (function (global){ 718 | 719 | /** 720 | * Parse `opts` to valid IDBKeyRange. 721 | * https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange 722 | * 723 | * @param {Object} opts 724 | * @return {IDBKeyRange} 725 | */ 726 | 727 | module.exports = function range(opts) { 728 | var IDBKeyRange = global.IDBKeyRange || global.webkitIDBKeyRange 729 | if (opts instanceof IDBKeyRange) return opts 730 | if (typeof opts === 'undefined') return null 731 | if (!isObject(opts)) return IDBKeyRange.only(opts) 732 | var keys = Object.keys(opts).sort() 733 | 734 | if (keys.length == 1) { 735 | var key = keys[0] 736 | var val = opts[key] 737 | 738 | switch (key) { 739 | case 'eq': return IDBKeyRange.only(val) 740 | case 'gt': return IDBKeyRange.lowerBound(val, true) 741 | case 'lt': return IDBKeyRange.upperBound(val, true) 742 | case 'gte': return IDBKeyRange.lowerBound(val) 743 | case 'lte': return IDBKeyRange.upperBound(val) 744 | default: throw new TypeError('`' + key + '` is not valid key') 745 | } 746 | } else { 747 | var x = opts[keys[0]] 748 | var y = opts[keys[1]] 749 | var pattern = keys.join('-') 750 | 751 | switch (pattern) { 752 | case 'gt-lt': return IDBKeyRange.bound(x, y, true, true) 753 | case 'gt-lte': return IDBKeyRange.bound(x, y, true, false) 754 | case 'gte-lt': return IDBKeyRange.bound(x, y, false, true) 755 | case 'gte-lte': return IDBKeyRange.bound(x, y, false, false) 756 | default: throw new TypeError('`' + pattern +'` are conflicted keys') 757 | } 758 | } 759 | } 760 | 761 | /** 762 | * Check if `obj` is an object (an even not an array). 763 | * 764 | * @param {Object} obj 765 | * @return {Boolean} 766 | */ 767 | 768 | function isObject(obj) { 769 | return Object.prototype.toString.call(obj) == '[object Object]' 770 | } 771 | 772 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 773 | },{}]},{},[4])(4) 774 | }); -------------------------------------------------------------------------------- /examples/es6-generators.js: -------------------------------------------------------------------------------- 1 | // ES6-generators is a powerful way to approach async workflow 2 | // This example uses [co](https://github.com/visionmedia/co) and treo-promise 3 | // to provide great readability. 4 | // It's a near future, because Chrome enabled ES6-Generators by default and 5 | // ES7 async/await proposal is basically the same approach. 6 | // Right now, we can compile generators with https://github.com/facebook/regenerator 7 | // and write nice code today. 8 | 9 | var treo = require('treo'); 10 | var promise = require('treo/plugins/treo-promise'); 11 | var co = require('co'); 12 | 13 | // define schema 14 | 15 | var schema = treo.schema() 16 | .version(1) 17 | .addStore('books') 18 | .addIndex('byTitle', 'title', { unique: true }) 19 | .addIndex('byAuthor', 'author') 20 | .addStore('locals') 21 | .version(2) 22 | .getStore('books') 23 | .addIndex('byYear', 'year') 24 | .version(3) 25 | .addStore('magazines', { key: 'id' }) 26 | .addIndex('byPublisher', 'publisher') 27 | .addIndex('byFrequency', 'frequency') 28 | .addIndex('byWords', 'words', { multi: true }); 29 | 30 | // create db with promises support 31 | 32 | var db = treo('library', schema) 33 | .use(promise()); 34 | 35 | // wrap async operations with generator. 36 | 37 | co(function*() { 38 | var books = db.store('books'); 39 | var magazines = db.store('magazines'); 40 | 41 | // load initial data 42 | 43 | yield books.batch({ 44 | 1: { title: 'Quarry Memories', author: 'Fred', isbn: 1, year: 2012 }, 45 | 2: { title: 'Water Buffaloes', author: 'Fred', isbn: 2, year: 2013 }, 46 | 3: { title: 'Bedrock Nights', author: 'Barney', isbn: 3, year: 2012 }, 47 | }); 48 | 49 | yield magazines.batch([ 50 | { id: 'id1', title: 'Quarry Memories', publisher: 'Bob' }, 51 | { id: 'id2', title: 'Water Buffaloes', publisher: 'Bob' }, 52 | { id: 'id3', title: 'Bedrocky Nights', publisher: 'Tim' }, 53 | { id: 'id4', title: 'Waving Wings', publisher: 'Ken' }, 54 | ]); 55 | 56 | // run queries 57 | 58 | var book = yield books.index('byTitle').get('Bedrock Nights'); 59 | console.log('Find book by unique index:', book); 60 | 61 | var byAuthor = yield books.index('byAuthor').get('Fred'); 62 | console.log('Filter books:', byAuthor); 63 | 64 | var magazinesCount = yield magazines.count(); 65 | console.log('Count magazines:', magazinesCount); 66 | }); 67 | -------------------------------------------------------------------------------- /examples/find-in-plugin.js: -------------------------------------------------------------------------------- 1 | // Example of simple treo plugin, which can be used as: 2 | // 3 | // var db = treo('library', schema) 4 | // .use(require('./find-in-plugin')); 5 | // 6 | // Treo ships with 2 plugins, and you can check them for more examples. 7 | 8 | module.exports = function plugin() { 9 | return function(db, treo) { 10 | var Index = treo.Index; 11 | var Store = treo.Store; 12 | 13 | /** 14 | * Efficient way to get bunch of records by `keys`. 15 | * Inspired by: https://hacks.mozilla.org/2014/06/breaking-the-borders-of-indexeddb/ 16 | * 17 | * Examples: 18 | * 19 | * var books = db.store('books'); 20 | * books.findIn(['book-1', 'book-2', 'book-n'], fn); 21 | * 22 | * var byAuthor = books.index('byAuthor'); 23 | * byAuthor.findIn(['Fred', 'Barney'], fn); 24 | * 25 | * SQL equivalent: 26 | * 27 | * SELECT * FROM BOOKS WHERE ID IN ('book-1', 'book-2', 'book-n') 28 | * SELECT * FROM BOOKS WHERE AUTHOR IN ('Fred', 'Barney') 29 | * 30 | * @param {Array} keys 31 | * @param {Function} cb - cb(err, result) 32 | */ 33 | 34 | Index.prototype.findIn = 35 | Store.prototype.findIn = function(keys, cb) { 36 | var result = []; 37 | var current = 0; 38 | keys = keys.sort(treo.cmp); 39 | 40 | this.cursor({ iterator: iterator }, done); 41 | 42 | function iterator(cursor) { 43 | if (current > keys.length) return done(); 44 | if (cursor.key > keys[current]) { 45 | result.push(undefined); // key not found 46 | current += 1; 47 | cursor.continue(keys[current]); 48 | } else if (cursor.key === keys[current]) { 49 | result.push(cursor.value); // key found 50 | current += 1; 51 | cursor.continue(keys[current]); 52 | } else { 53 | cursor.continue(keys[current]); // go to next key 54 | } 55 | } 56 | 57 | function done(err) { 58 | err ? cb(err) : cb(null, result); 59 | } 60 | }; 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /examples/key-value-storage.js: -------------------------------------------------------------------------------- 1 | // This example shows how to use treo for simple string key/value storage. 2 | // Often you don't need objects and indexes, and just need simple async 3 | // localStorage. With treo-websql it can be used in all modern browsers. 4 | 5 | var treo = require('treo'); 6 | var websql = require('treo/plugins/treo-websql'); 7 | var fn = console.log.bind(console); // use it as callback 8 | 9 | // define schema with one storage with string key/values 10 | var schema = treo.schema() 11 | .version(1) 12 | .addStore('storage'); 13 | 14 | // create db 15 | var db = treo('key-value-storage', schema) 16 | .use(websql()); // support legacy browsers 17 | 18 | // save link to storage 19 | var store = db.store('storage'); 20 | 21 | // put values 22 | store.put('foo', 'value 1', fn); 23 | store.put('bar', 'value 2', fn); 24 | store.put('baz', 'value 3', fn); 25 | 26 | // get value by key 27 | store.get('bar', fn); // 'value 2' 28 | 29 | // get all 30 | store.all(fn); // ['value 1', 'value 2', 'value 3'] 31 | 32 | // batch more records 33 | store.batch([ 34 | { 'key4': 'value 4' }, 35 | { 'key5': 'value 5' }, 36 | { 'key6': 'value 6' }, 37 | ], fn); 38 | 39 | // count 40 | store.count(fn); // 6 41 | 42 | // close db 43 | db.close(fn); 44 | -------------------------------------------------------------------------------- /lib/idb-index.js: -------------------------------------------------------------------------------- 1 | var parseRange = require('idb-range'); 2 | 3 | /** 4 | * Expose `Index`. 5 | */ 6 | 7 | module.exports = Index; 8 | 9 | /** 10 | * Initialize new `Index`. 11 | * 12 | * @param {Store} store 13 | * @param {String} name 14 | * @param {String|Array} field 15 | * @param {Object} opts { unique: false, multi: false } 16 | */ 17 | 18 | function Index(store, name, field, opts) { 19 | this.store = store; 20 | this.name = name; 21 | this.field = field; 22 | this.opts = opts; 23 | this.multi = opts.multi || opts.multiEntry || false; 24 | this.unique = opts.unique || false; 25 | } 26 | 27 | /** 28 | * Get `key`. 29 | * 30 | * @param {Object|IDBKeyRange} key 31 | * @param {Function} cb 32 | */ 33 | 34 | Index.prototype.get = function(key, cb) { 35 | var result = []; 36 | var isUnique = this.unique; 37 | var opts = { range: key, iterator: iterator }; 38 | 39 | this.cursor(opts, function(err) { 40 | if (err) return cb(err); 41 | isUnique ? cb(null, result[0]) : cb(null, result); 42 | }); 43 | 44 | function iterator(cursor) { 45 | result.push(cursor.value); 46 | cursor.continue(); 47 | } 48 | }; 49 | 50 | /** 51 | * Count records by `key`. 52 | * 53 | * @param {String|IDBKeyRange} key 54 | * @param {Function} cb 55 | */ 56 | 57 | Index.prototype.count = function(key, cb) { 58 | var name = this.store.name; 59 | var indexName = this.name; 60 | 61 | this.store.db.transaction('readonly', [name], function(err, tr) { 62 | if (err) return cb(err); 63 | var index = tr.objectStore(name).index(indexName); 64 | var req = index.count(parseRange(key)); 65 | req.onerror = cb; 66 | req.onsuccess = function onsuccess(e) { cb(null, e.target.result) }; 67 | }); 68 | }; 69 | 70 | /** 71 | * Create cursor. 72 | * Proxy to `this.store` for convinience. 73 | * 74 | * @param {Object} opts 75 | * @param {Function} cb 76 | */ 77 | 78 | Index.prototype.cursor = function(opts, cb) { 79 | opts.index = this.name; 80 | this.store.cursor(opts, cb); 81 | }; 82 | -------------------------------------------------------------------------------- /lib/idb-store.js: -------------------------------------------------------------------------------- 1 | var type = require('component-type'); 2 | var parseRange = require('idb-range'); 3 | 4 | /** 5 | * Expose `Store`. 6 | */ 7 | 8 | module.exports = Store; 9 | 10 | /** 11 | * Initialize new `Store`. 12 | * 13 | * @param {String} name 14 | * @param {Object} opts 15 | */ 16 | 17 | function Store(name, opts) { 18 | this.db = null; 19 | this.name = name; 20 | this.indexes = {}; 21 | this.opts = opts; 22 | this.key = opts.key || opts.keyPath || undefined; 23 | this.increment = opts.increment || opts.autoIncretement || undefined; 24 | } 25 | 26 | /** 27 | * Get index by `name`. 28 | * 29 | * @param {String} name 30 | * @return {Index} 31 | */ 32 | 33 | Store.prototype.index = function(name) { 34 | return this.indexes[name]; 35 | }; 36 | 37 | /** 38 | * Put (create or replace) `key` to `val`. 39 | * 40 | * @param {String|Object} [key] is optional when store.key exists. 41 | * @param {Any} val 42 | * @param {Function} cb 43 | */ 44 | 45 | Store.prototype.put = function(key, val, cb) { 46 | var name = this.name; 47 | var keyPath = this.key; 48 | if (keyPath) { 49 | if (type(key) == 'object') { 50 | cb = val; 51 | val = key; 52 | key = null; 53 | } else { 54 | val[keyPath] = key; 55 | } 56 | } 57 | 58 | this.db.transaction('readwrite', [name], function(err, tr) { 59 | if (err) return cb(err); 60 | var objectStore = tr.objectStore(name); 61 | var req = keyPath ? objectStore.put(val) : objectStore.put(val, key); 62 | tr.onerror = tr.onabort = req.onerror = cb; 63 | tr.oncomplete = function oncomplete() { cb(null, req.result) }; 64 | }); 65 | }; 66 | 67 | /** 68 | * Get `key`. 69 | * 70 | * @param {String} key 71 | * @param {Function} cb 72 | */ 73 | 74 | Store.prototype.get = function(key, cb) { 75 | var name = this.name; 76 | this.db.transaction('readonly', [name], function(err, tr) { 77 | if (err) return cb(err); 78 | var objectStore = tr.objectStore(name); 79 | var req = objectStore.get(key); 80 | req.onerror = cb; 81 | req.onsuccess = function onsuccess(e) { cb(null, e.target.result) }; 82 | }); 83 | }; 84 | 85 | /** 86 | * Del `key`. 87 | * 88 | * @param {String} key 89 | * @param {Function} cb 90 | */ 91 | 92 | Store.prototype.del = function(key, cb) { 93 | var name = this.name; 94 | this.db.transaction('readwrite', [name], function(err, tr) { 95 | if (err) return cb(err); 96 | var objectStore = tr.objectStore(name); 97 | var req = objectStore.delete(key); 98 | tr.onerror = tr.onabort = req.onerror = cb; 99 | tr.oncomplete = function oncomplete() { cb() }; 100 | }); 101 | }; 102 | 103 | /** 104 | * Count. 105 | * 106 | * @param {Function} cb 107 | */ 108 | 109 | Store.prototype.count = function(cb) { 110 | var name = this.name; 111 | this.db.transaction('readonly', [name], function(err, tr) { 112 | if (err) return cb(err); 113 | var objectStore = tr.objectStore(name); 114 | var req = objectStore.count(); 115 | req.onerror = cb; 116 | req.onsuccess = function onsuccess(e) { cb(null, e.target.result) }; 117 | }); 118 | }; 119 | 120 | /** 121 | * Clear. 122 | * 123 | * @param {Function} cb 124 | */ 125 | 126 | Store.prototype.clear = function(cb) { 127 | var name = this.name; 128 | this.db.transaction('readwrite', [name], function(err, tr) { 129 | if (err) return cb(err); 130 | var objectStore = tr.objectStore(name); 131 | var req = objectStore.clear(); 132 | tr.onerror = tr.onabort = req.onerror = cb; 133 | tr.oncomplete = function oncomplete() { cb() }; 134 | }); 135 | }; 136 | 137 | /** 138 | * Perform batch operation. 139 | * 140 | * @param {Object} vals 141 | * @param {Function} cb 142 | */ 143 | 144 | Store.prototype.batch = function(vals, cb) { 145 | var name = this.name; 146 | var keyPath = this.key; 147 | var keys = Object.keys(vals); 148 | 149 | this.db.transaction('readwrite', [name], function(err, tr) { 150 | if (err) return cb(err); 151 | var store = tr.objectStore(name); 152 | var current = 0; 153 | tr.onerror = tr.onabort = cb; 154 | tr.oncomplete = function oncomplete() { cb() }; 155 | next(); 156 | 157 | function next() { 158 | if (current >= keys.length) return; 159 | var currentKey = keys[current]; 160 | var currentVal = vals[currentKey]; 161 | var req; 162 | 163 | if (currentVal === null) { 164 | req = store.delete(currentKey); 165 | } else if (keyPath) { 166 | if (!currentVal[keyPath]) currentVal[keyPath] = currentKey; 167 | req = store.put(currentVal); 168 | } else { 169 | req = store.put(currentVal, currentKey); 170 | } 171 | 172 | req.onerror = cb; 173 | req.onsuccess = next; 174 | current += 1; 175 | } 176 | }); 177 | }; 178 | 179 | /** 180 | * Get all. 181 | * 182 | * @param {Function} cb 183 | */ 184 | 185 | Store.prototype.all = function(cb) { 186 | var result = []; 187 | 188 | this.cursor({ iterator: iterator }, function(err) { 189 | err ? cb(err) : cb(null, result); 190 | }); 191 | 192 | function iterator(cursor) { 193 | result.push(cursor.value); 194 | cursor.continue(); 195 | } 196 | }; 197 | 198 | /** 199 | * Create read cursor for specific `range`, 200 | * and pass IDBCursor to `iterator` function. 201 | * https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor 202 | * 203 | * @param {Object} opts: 204 | * {IDBRange|Object} range - passes to .openCursor() 205 | * {Function} iterator - function to call with IDBCursor 206 | * {String} [index] - name of index to start cursor by index 207 | * @param {Function} cb - calls on end or error 208 | */ 209 | 210 | Store.prototype.cursor = function(opts, cb) { 211 | var name = this.name; 212 | this.db.transaction('readonly', [name], function(err, tr) { 213 | if (err) return cb(err); 214 | var store = opts.index 215 | ? tr.objectStore(name).index(opts.index) 216 | : tr.objectStore(name); 217 | var req = store.openCursor(parseRange(opts.range)); 218 | 219 | req.onerror = cb; 220 | req.onsuccess = function onsuccess(e) { 221 | var cursor = e.target.result; 222 | cursor ? opts.iterator(cursor) : cb(); 223 | }; 224 | }); 225 | }; 226 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var type = require('component-type'); 2 | var Schema = require('./schema'); 3 | var Store = require('./idb-store'); 4 | var Index = require('./idb-index'); 5 | 6 | /** 7 | * Expose `Treo`. 8 | */ 9 | 10 | exports = module.exports = Treo; 11 | 12 | /** 13 | * Initialize new `Treo` instance. 14 | * 15 | * @param {String} name 16 | * @param {Schema} schema 17 | */ 18 | 19 | function Treo(name, schema) { 20 | if (!(this instanceof Treo)) return new Treo(name, schema); 21 | if (type(name) != 'string') throw new TypeError('`name` required'); 22 | if (!(schema instanceof Schema)) throw new TypeError('not valid schema'); 23 | 24 | this.name = name; 25 | this.status = 'close'; 26 | this.origin = null; 27 | this.stores = schema._stores; 28 | this.version = schema.getVersion(); 29 | this.onupgradeneeded = schema.callback(); 30 | 31 | // assign db property to each store 32 | Object.keys(this.stores).forEach(function(storeName) { 33 | this.stores[storeName].db = this; 34 | }, this); 35 | } 36 | 37 | /** 38 | * Expose core classes. 39 | */ 40 | 41 | exports.schema = Schema; 42 | exports.cmp = cmp; 43 | exports.Treo = Treo; 44 | exports.Schema = Schema; 45 | exports.Store = Store; 46 | exports.Index = Index; 47 | 48 | /** 49 | * Use plugin `fn`. 50 | * 51 | * @param {Function} fn 52 | * @return {Treo} 53 | */ 54 | 55 | Treo.prototype.use = function(fn) { 56 | fn(this, exports); 57 | return this; 58 | }; 59 | 60 | /** 61 | * Drop. 62 | * 63 | * @param {Function} cb 64 | */ 65 | 66 | Treo.prototype.drop = function(cb) { 67 | var name = this.name; 68 | this.close(function(err) { 69 | if (err) return cb(err); 70 | var req = indexedDB().deleteDatabase(name); 71 | req.onerror = cb; 72 | req.onsuccess = function onsuccess() { cb() }; 73 | }); 74 | }; 75 | 76 | /** 77 | * Close. 78 | * 79 | * @param {Function} cb 80 | */ 81 | 82 | Treo.prototype.close = function(cb) { 83 | if (this.status == 'close') return cb(); 84 | this.getInstance(function(err, db) { 85 | if (err) return cb(err); 86 | db.origin = null; 87 | db.status = 'close'; 88 | db.close(); 89 | cb(); 90 | }); 91 | }; 92 | 93 | /** 94 | * Get store by `name`. 95 | * 96 | * @param {String} name 97 | * @return {Store} 98 | */ 99 | 100 | Treo.prototype.store = function(name) { 101 | return this.stores[name]; 102 | }; 103 | 104 | /** 105 | * Get db instance. It starts opening transaction only once, 106 | * another requests will be scheduled to queue. 107 | * 108 | * @param {Function} cb 109 | */ 110 | 111 | Treo.prototype.getInstance = function(cb) { 112 | if (this.status == 'open') return cb(null, this.origin); 113 | if (this.status == 'opening') return this.queue.push(cb); 114 | 115 | this.status = 'opening'; 116 | this.queue = [cb]; // queue callbacks 117 | 118 | var that = this; 119 | var req = indexedDB().open(this.name, this.version); 120 | req.onupgradeneeded = this.onupgradeneeded; 121 | 122 | req.onerror = req.onblocked = function onerror(e) { 123 | that.status = 'error'; 124 | that.queue.forEach(function(cb) { cb(e) }); 125 | delete that.queue; 126 | }; 127 | 128 | req.onsuccess = function onsuccess(e) { 129 | that.origin = e.target.result; 130 | that.status = 'open'; 131 | that.origin.onversionchange = function onversionchange() { 132 | that.close(function() {}); 133 | }; 134 | that.queue.forEach(function(cb) { cb(null, that.origin) }); 135 | delete that.queue; 136 | }; 137 | }; 138 | 139 | /** 140 | * Create new transaction for selected `stores`. 141 | * 142 | * @param {String} type (readwrite|readonly) 143 | * @param {Array} stores - follow indexeddb semantic 144 | * @param {Function} cb 145 | */ 146 | 147 | Treo.prototype.transaction = function(type, stores, cb) { 148 | this.getInstance(function(err, db) { 149 | err ? cb(err) : cb(null, db.transaction(stores, type)); 150 | }); 151 | }; 152 | 153 | /** 154 | * Compare 2 values using IndexedDB comparision algotihm. 155 | * 156 | * @param {Mixed} value1 157 | * @param {Mixed} value2 158 | * @return {Number} -1|0|1 159 | */ 160 | 161 | function cmp() { 162 | return indexedDB().cmp.apply(indexedDB(), arguments); 163 | } 164 | 165 | /** 166 | * Dynamic link to `global.indexedDB` for polyfills support. 167 | * 168 | * @return {IDBDatabase} 169 | */ 170 | 171 | function indexedDB() { 172 | return global._indexedDB 173 | || global.indexedDB 174 | || global.msIndexedDB 175 | || global.mozIndexedDB 176 | || global.webkitIndexedDB; 177 | } 178 | -------------------------------------------------------------------------------- /lib/schema.js: -------------------------------------------------------------------------------- 1 | var type = require('component-type'); 2 | var Store = require('./idb-store'); 3 | var Index = require('./idb-index'); 4 | 5 | /** 6 | * Expose `Schema`. 7 | */ 8 | 9 | module.exports = Schema; 10 | 11 | /** 12 | * Initialize new `Schema`. 13 | */ 14 | 15 | function Schema() { 16 | if (!(this instanceof Schema)) return new Schema(); 17 | this._stores = {}; 18 | this._current = {}; 19 | this._versions = {}; 20 | } 21 | 22 | /** 23 | * Set new version. 24 | * 25 | * @param {Number} version 26 | * @return {Schema} 27 | */ 28 | 29 | Schema.prototype.version = function(version) { 30 | if (type(version) != 'number' || version < 1 || version < this.getVersion()) 31 | throw new TypeError('not valid version'); 32 | 33 | this._current = { version: version, store: null }; 34 | this._versions[version] = { 35 | stores: [], // db.createObjectStore 36 | dropStores: [], // db.deleteObjectStore 37 | indexes: [], // store.createIndex 38 | dropIndexes: [], // store.deleteIndex 39 | version: version // version 40 | }; 41 | 42 | return this; 43 | }; 44 | 45 | /** 46 | * Add store. 47 | * 48 | * @param {String} name 49 | * @param {Object} [opts] { key: false } 50 | * @return {Schema} 51 | */ 52 | 53 | Schema.prototype.addStore = function(name, opts) { 54 | if (type(name) != 'string') throw new TypeError('`name` is required'); 55 | if (this._stores[name]) throw new TypeError('store is already defined'); 56 | var store = new Store(name, opts || {}); 57 | this._stores[name] = store; 58 | this._versions[this.getVersion()].stores.push(store); 59 | this._current.store = store; 60 | return this; 61 | }; 62 | 63 | /** 64 | * Drop store. 65 | * 66 | * @param {String} name 67 | * @return {Schema} 68 | */ 69 | 70 | Schema.prototype.dropStore = function(name) { 71 | if (type(name) != 'string') throw new TypeError('`name` is required'); 72 | var store = this._stores[name]; 73 | if (!store) throw new TypeError('store is not defined'); 74 | delete this._stores[name]; 75 | this._versions[this.getVersion()].dropStores.push(store); 76 | return this; 77 | }; 78 | 79 | /** 80 | * Add index. 81 | * 82 | * @param {String} name 83 | * @param {String|Array} field 84 | * @param {Object} [opts] { unique: false, multi: false } 85 | * @return {Schema} 86 | */ 87 | 88 | Schema.prototype.addIndex = function(name, field, opts) { 89 | if (type(name) != 'string') throw new TypeError('`name` is required'); 90 | if (type(field) != 'string' && type(field) != 'array') throw new TypeError('`field` is required'); 91 | var store = this._current.store; 92 | if (store.indexes[name]) throw new TypeError('index is already defined'); 93 | var index = new Index(store, name, field, opts || {}); 94 | store.indexes[name] = index; 95 | this._versions[this.getVersion()].indexes.push(index); 96 | return this; 97 | }; 98 | 99 | /** 100 | * Drop index. 101 | * 102 | * @param {String} name 103 | * @return {Schema} 104 | */ 105 | 106 | Schema.prototype.dropIndex = function(name) { 107 | if (type(name) != 'string') throw new TypeError('`name` is required'); 108 | var index = this._current.store.indexes[name]; 109 | if (!index) throw new TypeError('index is not defined'); 110 | delete this._current.store.indexes[name]; 111 | this._versions[this.getVersion()].dropIndexes.push(index); 112 | return this; 113 | }; 114 | 115 | /** 116 | * Change current store. 117 | * 118 | * @param {String} name 119 | * @return {Schema} 120 | */ 121 | 122 | Schema.prototype.getStore = function(name) { 123 | if (type(name) != 'string') throw new TypeError('`name` is required'); 124 | if (!this._stores[name]) throw new TypeError('store is not defined'); 125 | this._current.store = this._stores[name]; 126 | return this; 127 | }; 128 | 129 | /** 130 | * Get version. 131 | * 132 | * @return {Number} 133 | */ 134 | 135 | Schema.prototype.getVersion = function() { 136 | return this._current.version; 137 | }; 138 | 139 | /** 140 | * Generate onupgradeneeded callback. 141 | * 142 | * @return {Function} 143 | */ 144 | 145 | Schema.prototype.callback = function() { 146 | var versions = Object.keys(this._versions) 147 | .map(function(v) { return this._versions[v] }, this) 148 | .sort(function(a, b) { return a.version - b.version }); 149 | 150 | return function onupgradeneeded(e) { 151 | var db = e.target.result; 152 | var tr = e.target.transaction; 153 | 154 | versions.forEach(function(versionSchema) { 155 | if (e.oldVersion >= versionSchema.version) return; 156 | 157 | versionSchema.stores.forEach(function(s) { 158 | var options = {}; 159 | 160 | // Only pass the options that are explicitly specified to createObjectStore() otherwise IE/Edge 161 | // can throw an InvalidAccessError - see https://msdn.microsoft.com/en-us/library/hh772493(v=vs.85).aspx 162 | if (typeof s.key !== 'undefined') options.keyPath = s.key; 163 | if (typeof s.increment !== 'undefined') options.autoIncrement = s.increment; 164 | 165 | db.createObjectStore(s.name, options); 166 | }); 167 | 168 | versionSchema.dropStores.forEach(function(s) { 169 | db.deleteObjectStore(s.name); 170 | }); 171 | 172 | versionSchema.indexes.forEach(function(i) { 173 | var store = tr.objectStore(i.store.name); 174 | store.createIndex(i.name, i.field, { 175 | unique: i.unique, 176 | multiEntry: i.multi 177 | }); 178 | }); 179 | 180 | versionSchema.dropIndexes.forEach(function(i) { 181 | var store = tr.objectStore(i.store.name); 182 | store.deleteIndex(i.name); 183 | }); 184 | }); 185 | }; 186 | }; 187 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "treo", 3 | "description": "Human interface for IndexedDB", 4 | "repository": "git@github.com:treojs/treo.git", 5 | "version": "0.5.1", 6 | "license": "MIT", 7 | "main": "lib/index.js", 8 | "keywords": [ 9 | "indexeddb", 10 | "treo", 11 | "offline" 12 | ], 13 | "scripts": { 14 | "test": "browserify-test", 15 | "start": "browserify-test -w", 16 | "stat": "cloc lib/ --by-file && cloc test/ --by-file --exclude-dir=fixtures", 17 | "build": "npm run build-treo && npm run build-treo-websql && npm run build-treo-promise", 18 | "build-treo": "browserify lib -s treo -o dist/treo.js", 19 | "build-treo-websql": "browserify plugins/treo-promise -s treoPromise -o dist/treo-promise.js", 20 | "build-treo-promise": "browserify plugins/treo-websql -s treoWebsql -o dist/treo-websql.js" 21 | }, 22 | "dependencies": { 23 | "component-type": "1.1.0", 24 | "idb-range": "^3.0.0", 25 | "promise": "^7.0.4" 26 | }, 27 | "devDependencies": { 28 | "after": "^0.8.1", 29 | "browserify-test": "^2.0.0", 30 | "browserify": "^12.0.1", 31 | "chai": "^3.4.0" 32 | }, 33 | "files": [ 34 | "lib", 35 | "plugins" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /plugins/treo-promise/index.js: -------------------------------------------------------------------------------- 1 | var denodeify = require('promise').denodeify; 2 | 3 | /** 4 | * Expose `plugin()`. 5 | */ 6 | 7 | module.exports = plugin; 8 | 9 | /** 10 | * Methods for patch. 11 | */ 12 | 13 | var dbMethods = [ 14 | ['drop', 1], 15 | ['close', 1] 16 | ]; 17 | 18 | var storeMethods = [ 19 | ['put', 3], 20 | ['get', 2], 21 | ['del', 2], 22 | ['count', 1], 23 | ['clear', 1], 24 | ['batch', 2], 25 | ['all', 1], 26 | ]; 27 | 28 | var indexMethods = [ 29 | ['get', 2], 30 | ['count', 2], 31 | ]; 32 | 33 | /** 34 | * Denodeify each db's method and add promises support with 35 | * https://github.com/jakearchibald/es6-promise 36 | */ 37 | 38 | function plugin() { 39 | return function(db) { 40 | patch(db, dbMethods); 41 | 42 | Object.keys(db.stores).forEach(function(storeName) { 43 | var store = db.store(storeName); 44 | patch(store, storeMethods); 45 | 46 | Object.keys(store.indexes).forEach(function(indexName) { 47 | var index = store.index(indexName); 48 | patch(index, indexMethods); 49 | }); 50 | }); 51 | }; 52 | } 53 | 54 | /** 55 | * Patch `methods` from `object` with `denodeify`. 56 | * 57 | * @param {Object} object 58 | * @param {Array} methods 59 | */ 60 | 61 | function patch(object, methods) { 62 | methods.forEach(function(m) { 63 | object[m[0]] = denodeify(object[m[0]], m[1]); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /plugins/treo-websql/index.js: -------------------------------------------------------------------------------- 1 | var parseRange = require('idb-range'); 2 | var isSafari = typeof window.openDatabase !== 'undefined' && 3 | /Safari/.test(navigator.userAgent) && 4 | !/Chrome/.test(navigator.userAgent); 5 | 6 | var isSupported = !isSafari && !! window.indexedDB; 7 | 8 | /** 9 | * Expose `plugin()`. 10 | */ 11 | 12 | module.exports = plugin; 13 | 14 | /** 15 | * Create websql polyfill based on: 16 | * https://github.com/axemclion/IndexedDBShim 17 | * And fix some polyfill's bugs as: 18 | * - multi index support 19 | * 20 | * @return {Function} 21 | */ 22 | 23 | function plugin() { 24 | if (!isSupported) require('./indexeddb-shim'); 25 | 26 | return function(db, treo) { 27 | if (isSupported) return; 28 | // fix multi index support 29 | // https://github.com/axemclion/IndexedDBShim/issues/16 30 | Object.keys(db.stores).forEach(function(storeName) { 31 | var store = db.store(storeName); 32 | Object.keys(store.indexes).forEach(function(indexName) { 33 | var index = store.index(indexName); 34 | fixIndexSupport(treo, index); 35 | }); 36 | }); 37 | }; 38 | } 39 | 40 | /** 41 | * Patch `index` to support multi property with websql polyfill. 42 | * 43 | * @param {Index} index 44 | */ 45 | 46 | function fixIndexSupport(treo, index) { 47 | index.get = function get(key, cb) { 48 | console.warn('treo-websql: index is enefficient'); 49 | var result = []; 50 | var r = parseRange(key); 51 | 52 | this.store.cursor({ iterator: iterator }, function(err) { 53 | err ? cb(err) : cb(null, index.unique ? result[0] : result); 54 | }); 55 | 56 | function iterator(cursor) { 57 | var field; 58 | if (Array.isArray(index.field)) { 59 | field = index.field.map(function(field) { 60 | return cursor.value[field]; 61 | }); 62 | } else { 63 | field = cursor.value[index.field]; 64 | } 65 | if (index.multi) { 66 | if (Array.isArray(field)) { 67 | field.forEach(function(v) { 68 | if (testValue(v)) result.push(cursor.value); 69 | }); 70 | } 71 | } else if (field !== undefined) { 72 | if (testValue(field)) result.push(cursor.value); 73 | } 74 | cursor.continue(); 75 | } 76 | 77 | function testValue(v) { 78 | return (((!r.lowerOpen && v >= r.lower) || (r.lowerOpen && v > r.lower)) && ((!r.upperOpen && v <= r.upper) || (r.upperOpen && v < r.upper)) 79 | || (r.upper === undefined && ((!r.lowerOpen && v >= r.lower) || (r.lowerOpen && v > r.lower))) 80 | || (r.lower === undefined && ((!r.upperOpen && v <= r.upper) || (r.upperOpen && v < r.upper)))); 81 | } 82 | }; 83 | 84 | index.count = function count(key, cb) { 85 | this.get(key, function(err, result) { 86 | err ? cb(err) : cb(null, index.unique && result ? 1 : result.length); 87 | }); 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | /* globals after */ 2 | var expect = require('chai').expect; 3 | var Promise = require('promise'); 4 | var treo = require('../lib'); 5 | var websql = require('../plugins/treo-websql'); 6 | var promise = require('../plugins/treo-promise'); 7 | 8 | describe('integration', function() { 9 | this.timeout(10000); 10 | var db, modules; 11 | 12 | before(function(done) { 13 | var data = require('./fixtures/npm-data.json'); 14 | var schema = treo.schema() 15 | .version(1) 16 | .addStore('modules', { keyPath: 'name' }) 17 | .version(2) 18 | .getStore('modules') 19 | .addIndex('byKeywords', 'keywords', { multiEntry: true }) 20 | .addIndex('byAuthor', 'author') 21 | .addIndex('byStars', 'stars') 22 | .addIndex('byMaintainers', 'maintainers', { multi: true }); 23 | 24 | db = treo('npm', schema) 25 | .use(websql()) 26 | .use(promise()); 27 | 28 | modules = db.store('modules'); 29 | modules.batch(data, done); 30 | }); 31 | 32 | after(function(done) { 33 | db.drop(done); 34 | }); 35 | 36 | it('get module', function(done) { 37 | modules.get('browserify', function(err, mod) { 38 | if (err) return done(err); 39 | expect(mod).exist; 40 | expect(mod.author).equal('James Halliday'); 41 | done(); 42 | }); 43 | }); 44 | 45 | it('count all modules', function(done) { 46 | modules.count(function(err, count) { 47 | if (err) return done(err); 48 | expect(count).equal(473); 49 | done(); 50 | }); 51 | }); 52 | 53 | it('count by index', function(done) { 54 | modules.index('byStars').count({ gte: 100 }, function(err, count) { 55 | expect(count).equal(12); 56 | modules.index('byKeywords').count('grunt', function(err2, count) { 57 | expect(count).equal(9); 58 | modules.index('byMaintainers').count('tjholowaychuk', function(err3, count) { 59 | expect(count).equal(36); 60 | done(err || err2 || err3); 61 | }); 62 | }); 63 | }); 64 | }); 65 | 66 | it('works with promises', function(done) { 67 | Promise.all([ 68 | modules.get('async'), 69 | modules.get('request'), 70 | modules.get('component'), 71 | ]).then(function(records) { 72 | expect(records).length(3); 73 | done(); 74 | }, done); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/treo.js: -------------------------------------------------------------------------------- 1 | /* globals -after */ 2 | var expect = require('chai').expect; 3 | var after = require('after'); 4 | var treo = require('../lib'); 5 | var websql = require('../plugins/treo-websql'); 6 | 7 | describe('treo', function() { 8 | var db, schema; 9 | 10 | beforeEach(function() { 11 | schema = treo.schema() 12 | .version(1) 13 | .addStore('books') 14 | .addIndex('byTitle', 'title', { unique: true }) 15 | .addIndex('byAuthor', 'author') 16 | .addIndex('byTitleAndAuthor', ['title', 'author'], { unique: true }) 17 | .addStore('locals') 18 | .version(2) 19 | .getStore('books') 20 | .addIndex('byYear', 'year') 21 | .version(3) 22 | .addStore('magazines', { key: 'id' }) 23 | .addIndex('byPublisher', 'publisher') 24 | .addIndex('byFrequency', 'frequency') 25 | .addIndex('byWords', 'words', { multi: true }); 26 | db = treo('treo', schema).use(websql()); 27 | }); 28 | 29 | afterEach(function(done) { 30 | db.drop(done); 31 | }); 32 | 33 | describe('db', function() { 34 | it('has properties', function() { 35 | expect(db.name).equal('treo'); 36 | expect(db.version).equal(3); 37 | expect(db.status).equal('close'); 38 | expect(Object.keys(db.stores)).length(3); 39 | }); 40 | 41 | it('parallel read', function(done) { 42 | var next = after(3, done); 43 | db.store('books').count(next); 44 | db.store('magazines').count(next); 45 | db.store('magazines').index('byPublisher').get(1, next); 46 | }); 47 | 48 | it('parallel write', function(done) { 49 | var books = db.store('books'); 50 | var magazines = db.store('magazines'); 51 | var next = after(4, function() { 52 | books.all(function(err, records) { 53 | expect(records).length(3); 54 | magazines.count(function(err2, count) { 55 | expect(count).equal(1); 56 | done(err || err2); 57 | }); 58 | }); 59 | }); 60 | 61 | books.batch({ 1: { name: 'book 1' }, 2: { id: 2, name: 'book 2' } }, next); 62 | books.put(3, { id: 3, name: 'book 3' }, next); 63 | magazines.del(5, next); 64 | magazines.put({ id: 4, message: 'hey' }, next); 65 | }); 66 | 67 | it('drop stores', function(done) { 68 | var dropSchema = treo.schema() 69 | .version(1) 70 | .addStore('books') 71 | .addIndex('byTitle', 'title', { unique: true }) 72 | .addIndex('byAuthor', 'author') 73 | .addStore('locals') 74 | .version(2) 75 | .addStore('magazines', { key: 'id' }) 76 | .addIndex('byWords', 'words', { multi: true }); 77 | 78 | var db = treo('treo', dropSchema).use(websql()); 79 | db.store('magazines').put({ id: 4, words: ['hey'] }, function(err) { 80 | if (err) return done(err); 81 | db.close(function() { 82 | dropSchema = dropSchema.version(3) 83 | .dropStore('books') 84 | .getStore('magazines') 85 | .dropIndex('byWords'); 86 | var db = treo('treo', dropSchema).use(websql()); 87 | expect(Object.keys(db.stores)).length(2); 88 | expect(Object.keys(db.store('magazines').indexes)).length(0); 89 | 90 | db.store('magazines').get(4, function(err, obj) { 91 | if (err) return done(err); 92 | expect(obj).eql({ id: 4, words: ['hey'] }); 93 | db.drop(done); 94 | }); 95 | }); 96 | }); 97 | }); 98 | 99 | it('handlew onversionchange automatically', function(done) { 100 | db.store('magazines').put({ id: 4, words: ['hey'] }, function(err) { 101 | if (err) return done(err); 102 | var newSchema = schema.version(4).addStore('users'); 103 | var newDb = treo('treo', newSchema).use(websql()); 104 | newDb.store('users').put(1, { name: 'Jon'}, function(err, key) { 105 | if (err) return done(err); 106 | expect(key).equal(1); 107 | 108 | db.store('magazines').get(4, function(err, obj) { 109 | if (err) return done(err); 110 | expect(obj).eql({ id: 4, words: ['hey'] }); 111 | done(); 112 | }); 113 | }); 114 | }); 115 | }); 116 | }); 117 | 118 | describe('store', function() { 119 | it('has properties', function() { 120 | var books = db.store('books'); 121 | var magazines = db.store('magazines'); 122 | expect(books.name).equal('books'); 123 | expect(books.db).equal(db); 124 | expect(Object.keys(books.indexes)).length(4); 125 | expect(magazines.key).equal('id'); 126 | expect(magazines.opts.key).equal('id'); 127 | }); 128 | 129 | it('#put', function(done) { 130 | var attrs = { title: 'Quarry Memories', author: 'Fred', isbn: 123456 }; 131 | var books = db.store('books'); 132 | var magazines = db.store('magazines'); 133 | var next = after(2, done); 134 | 135 | books.put(attrs.isbn, attrs, function(err, key) { 136 | if (err) return done(err); 137 | expect(key).equal(123456); 138 | books.get(attrs.isbn, function(err, book) { 139 | if (err) return done(err); 140 | expect(book).eql(attrs); 141 | next(); 142 | }); 143 | }); 144 | 145 | magazines.put('id1', { name: 'new magazine' }, function(err) { 146 | if (err) return done(err); 147 | magazines.get('id1', function(err, magazine) { 148 | if (err) return done(err); 149 | expect(magazine.id).eql('id1'); 150 | next(); 151 | }); 152 | }); 153 | }); 154 | 155 | it('#clear', function(done) { 156 | var books = db.store('books'); 157 | books.batch({ 158 | '123456': { title: 'Quarry Memories', author: 'Fred', isbn: '123456' }, 159 | '234567': { title: 'Water Buffaloes', author: 'Fred', isbn: '234567' }, 160 | }, function(err) { 161 | books.count(function(err2, count) { 162 | expect(count).equal(2); 163 | books.clear(function(err3) { 164 | books.count(function(err4, count) { 165 | expect(count).equal(0); 166 | done(err || err2 || err3 || err4); 167 | }); 168 | }); 169 | }); 170 | }); 171 | }); 172 | 173 | it('#del', function(done) { 174 | var magazines = db.store('magazines'); 175 | magazines.batch({ 176 | 'id1': { title: 'Quarry Memories', publisher: 'Bob' }, 177 | 'id2': { title: 'Water Buffaloes', publisher: 'Bob' }, 178 | 'id3': { title: 'Bedrocky Nights', publisher: 'Tim' }, 179 | 'id4': { title: 'Heavy weighting', publisher: 'Ken' }, 180 | }, function(err) { 181 | if (err) return done(err); 182 | magazines.del('id1', function(err) { 183 | if (err) return done(err); 184 | magazines.count(function(err, count) { 185 | expect(count).equal(3); 186 | done(err); 187 | }); 188 | }); 189 | }); 190 | }); 191 | 192 | it('#all', function(done) { 193 | var magazines = db.store('magazines'); 194 | magazines.batch([ 195 | { id: 'id1', title: 'Quarry Memories', publisher: 'Bob' }, 196 | { id: 'id2', title: 'Water Buffaloes', publisher: 'Bob' }, 197 | { id: 'id3', title: 'Bedrocky Nights', publisher: 'Tim' }, 198 | { id: 'id4', title: 'Waving Wings', publisher: 'Ken' }, 199 | ], function(err) { 200 | if (err) return done(err); 201 | 202 | magazines.all(function(err, result) { 203 | expect(result).length(4); 204 | expect(result[0].id).equal('id1'); 205 | done(err); 206 | }); 207 | }); 208 | }); 209 | 210 | it('#batch', function(done) { 211 | var magazines = db.store('magazines'); 212 | magazines.batch({ 213 | 'id1': { title: 'Quarry Memories', publisher: 'Bob' }, 214 | 'id2': { title: 'Water Buffaloes', publisher: 'Bob' }, 215 | }, function(err) { 216 | if (err) return done(err); 217 | magazines.batch({ 218 | 'id1': null, 219 | 'id3': { title: 'Bedrocky Nights', publisher: 'Tim' }, 220 | 'id4': { title: 'Heavy Weighting', publisher: 'Ken' }, 221 | 'id2': null, 222 | }, function(err) { 223 | if (err) return done(err); 224 | magazines.count(function(err, count) { 225 | expect(count).equal(2); 226 | done(err); 227 | }); 228 | }); 229 | }); 230 | }); 231 | }); 232 | 233 | describe('index', function() { 234 | var books; 235 | 236 | beforeEach(function(done) { 237 | books = db.store('books'); 238 | books.batch({ 239 | 1: { title: 'Quarry Memories', author: 'Fred', isbn: 1, year: 2012 }, 240 | 2: { title: 'Water Buffaloes', author: 'Fred', isbn: 2, year: 2013 }, 241 | 3: { title: 'Bedrock Nights', author: 'Barney', isbn: 3, year: 2012 }, 242 | }, done); 243 | }); 244 | 245 | it('has properties', function() { 246 | var byAuthor = books.index('byAuthor'); 247 | expect(byAuthor.name).equal('byAuthor'); 248 | expect(byAuthor.unique).false; 249 | expect(byAuthor.multi).false; 250 | expect(byAuthor.field).equal('author'); 251 | expect(byAuthor.store).equal(books); 252 | }); 253 | 254 | it('#get by not unique index', function(done) { 255 | books.index('byAuthor').get('Fred', function(err, records) { 256 | if (err) return done(err); 257 | expect(records).length(2); 258 | expect(records[0].isbn).equal(1); 259 | 260 | books.index('byYear').get(2013, function(err, records) { 261 | expect(records).length(1); 262 | expect(records[0].isbn).equal(2); 263 | done(err); 264 | }); 265 | }); 266 | }); 267 | 268 | it('#get with object params', function(done) { 269 | books.index('byYear').get({ gte: 2012 }, function(err, all) { 270 | expect(all).length(3); 271 | 272 | books.index('byYear').get({ gt: 2012, lte: 2013 }, function(err, all) { 273 | expect(all).length(1); 274 | done(err); 275 | }); 276 | }); 277 | }); 278 | 279 | it('#get by unique index', function(done) { 280 | books.index('byTitle').get('Bedrock Nights', function(err, val) { 281 | if (err) return done(err); 282 | expect(val.isbn).equal(3); 283 | expect(val.title).equal('Bedrock Nights'); 284 | expect(Object.keys(val)).length(4); 285 | done(); 286 | }); 287 | }); 288 | 289 | it('#count', function(done) { 290 | books.index('byYear').count(2012, function(err, count) { 291 | if (err) return done(err); 292 | expect(count).equal(2); 293 | 294 | books.index('byTitle').count('Water Buffaloes', function(err, count) { 295 | if (err) return done(err); 296 | expect(count).equal(1); 297 | done(); 298 | }); 299 | }); 300 | }); 301 | 302 | it('multi index', function(done) { 303 | var magazines = db.store('magazines'); 304 | magazines.batch({ 305 | 'id1': { title: 'Quarry Memories', words: ['quarry', 'memories'] }, 306 | 'id2': { title: 'Water Bad Fellows', words: ['water', 'bad', 'fellows'] }, 307 | 'id3': { title: 'Badrocky Nights', words: ['badrocky', 'nights'] }, 308 | 'id4': { title: 'Waving Wings', words: ['waving', 'wings'] }, 309 | }, function(err) { 310 | if (err) return done(err); 311 | var next = after(2, done); 312 | 313 | magazines.index('byWords').get({ gte: 'bad', lte: 'bad\uffff' }, function(err, result) { 314 | expect(result).length(2); 315 | expect(result[0].id).equal('id2'); 316 | next(err); 317 | }); 318 | 319 | magazines.index('byWords').get({ gte: 'w', lte: 'w\uffff' }, function(err, result) { 320 | expect(result).length(3); 321 | expect(result[1].id).equal('id4'); 322 | expect(result[2].id).equal('id4'); 323 | next(err); 324 | }); 325 | }); 326 | }); 327 | 328 | it('compound multi-field index', function(done) { 329 | books.index('byTitleAndAuthor').get(['Quarry Memories', 'Fred'], function(err, record) { 330 | if (err) return done(err); 331 | expect(record).exist; 332 | expect(record.isbn).equal(1); 333 | done(); 334 | }); 335 | }); 336 | }); 337 | 338 | describe('non objects', function() { 339 | it('#put val to key', function(done) { 340 | var locals = db.store('locals'); 341 | locals.put('foo', 'bar', function(err) { 342 | if (err) return done(err); 343 | locals.get('foo', function(err, val) { 344 | expect(val).equal('bar'); 345 | done(err); 346 | }); 347 | }); 348 | }); 349 | 350 | it('#batch many records', function(done) { 351 | var locals = db.store('locals'); 352 | locals.batch({ 353 | foo: 'val 1', 354 | bar: 'val 2', 355 | baz: 'val 3', 356 | }, function(err) { 357 | if (err) return done(err); 358 | var next = after(3, done); 359 | 360 | locals.get('bar', function(err, val) { 361 | expect(val).equal('val 2'); 362 | next(err); 363 | }); 364 | 365 | locals.count(function(err, count) { 366 | expect(count).equal(3); 367 | next(err); 368 | }); 369 | 370 | locals.get('fake', function(err, val) { 371 | expect(val).not.exist; 372 | next(err); 373 | }); 374 | }); 375 | }); 376 | }); 377 | }); 378 | --------------------------------------------------------------------------------