├── .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://npmjs.org/package/treo)
4 | [](https://travis-ci.org/treojs/treo)
5 | [](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 |
--------------------------------------------------------------------------------