├── .gitignore ├── example ├── multiple_idb_stores │ ├── index.html │ └── index.dart └── simple_run_through │ ├── index.html │ └── index.dart ├── analysis_options.yaml ├── pubspec.yaml ├── LICENSE ├── lib ├── src │ ├── memory_store.dart │ ├── local_storage_store.dart │ ├── _map_store.dart │ ├── indexeddb_store.dart │ └── websql_store.dart └── lawndart.dart ├── CHANGELOG.md ├── README.md └── test └── lawndart_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .children 2 | .project 3 | packages 4 | /pubspec.lock 5 | .buildlog 6 | build.snapshot 7 | out 8 | test/*.js* 9 | .idea 10 | 11 | .packages 12 | .pub 13 | .dart_tool -------------------------------------------------------------------------------- /example/multiple_idb_stores/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | index 6 | 7 | 8 | 9 |

10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | # exclude: 3 | # - path/to/excluded/files/** 4 | 5 | # Lint rules and documentation, see http://dart-lang.github.io/linter/lints 6 | linter: 7 | rules: 8 | - cancel_subscriptions 9 | - close_sinks 10 | - hash_and_equals 11 | - iterable_contains_unrelated_type 12 | - list_remove_unrelated_type 13 | - test_types_in_equals 14 | - unrelated_type_equality_checks 15 | - valid_regexps 16 | -------------------------------------------------------------------------------- /example/multiple_idb_stores/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:lawndart/lawndart.dart'; 2 | import 'dart:html'; 3 | 4 | main() async { 5 | await window.indexedDB.deleteDatabase('temptestdb'); 6 | Store store = await Store.open('temptestdb', 'store1'); 7 | print('opened 1'); 8 | await Store.open('temptestdb', 'store2'); 9 | print('opened 2'); 10 | await store.all().toList(); 11 | print('all done'); 12 | querySelector('#text').text = 'all done'; 13 | } 14 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: lawndart 2 | version: 0.10.0 3 | author: Seth Ladd 4 | description: An easier way to access storage APIs in the browser. 5 | homepage: https://github.com/sethladd/lawndart 6 | documentation: http://sethladd.github.com/lawndart/ 7 | 8 | environment: 9 | sdk: '>=2.0.0-dev.67.0 <3.0.0' 10 | 11 | dev_dependencies: 12 | build_runner: ^0.9.1 13 | build_test: ^0.10.3 14 | build_web_compilers: ^0.4.0+4 15 | test: ^1.3.0 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/simple_run_through/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lawndart - Simple Run Through 6 | 7 | 8 | 9 |

10 |

11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Google 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /lib/src/memory_store.dart: -------------------------------------------------------------------------------- 1 | //Copyright 2012 Google 2 | // 3 | //Licensed under the Apache License, Version 2.0 (the "License"); 4 | //you may not use this file except in compliance with the License. 5 | //You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | //Unless required by applicable law or agreed to in writing, software 10 | //distributed under the License is distributed on an "AS IS" BASIS, 11 | //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | //See the License for the specific language governing permissions and 13 | //limitations under the License. 14 | 15 | part of lawndart; 16 | 17 | class MemoryStore extends _MapStore { 18 | MemoryStore._() : super._(); 19 | 20 | static Future open() async { 21 | var store = new MemoryStore._(); 22 | await store._open(); 23 | return store; 24 | } 25 | 26 | @override 27 | Map _generateMap() => new Map(); 28 | } 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.10.0 2 | 3 | * Dart 2 compliance 4 | 5 | ## 0.9.0 6 | 7 | * BREAKING CHANGE: creating and opening a store are the same operation 8 | open() is a static method on Store 9 | * Using the new async/await/async*/yield features. 10 | * BREAKING CHANGE: No more generics. Stores are simply String => String stores now. 11 | 12 | ## 0.6.5 13 | 14 | * Reduce size of WebSQL down to 4MB, avoids permission check. 15 | 16 | ## 0.6.2 17 | 18 | * Update to SDK version 0.8.5 19 | * Remove old web_ui example 20 | 21 | ## 0.6.1 22 | 23 | * Fix bug with chained opens of multiple stores, followed by a read. 24 | 25 | ## 0.6.0 26 | 27 | * No more explicit version for indexed, it's automatically handled. 28 | * Better support for multiple store names per IndexedDB. 29 | * Thanks to https://github.com/davidB 30 | 31 | ## 0.5.0 32 | 33 | * Added factory constructor to automatically choose the best store. 34 | * Updated to hop standalone. 35 | 36 | ## 0.4.2 37 | 38 | * Added IndexedDbStore.supported 39 | * Added WebSqlStore.supported 40 | * Renamed all the adapters to stores 41 | * The TODO sample app now works in Safari, Chrome, and Firefox 42 | -------------------------------------------------------------------------------- /example/simple_run_through/index.dart: -------------------------------------------------------------------------------- 1 | library index; 2 | 3 | import 'package:lawndart/lawndart.dart'; 4 | import 'dart:html'; 5 | import 'dart:web_sql'; 6 | import 'dart:indexed_db'; 7 | 8 | runThrough(Store store, String id) async { 9 | var elem = querySelector('#$id'); 10 | 11 | try { 12 | await store.nuke(); 13 | await store.save(id, "hello"); 14 | await store.save("is fun", "dart"); 15 | await for (var value in store.all()) { 16 | elem.appendText('$value, '); 17 | } 18 | elem.appendText('all done'); 19 | } catch (e) { 20 | elem.text = e.toString(); 21 | } 22 | } 23 | 24 | main() async { 25 | if (SqlDatabase.supported) { 26 | var store = await WebSqlStore.open('test', 'test'); 27 | runThrough(store, 'websql'); 28 | } else { 29 | querySelector('#websql').text = 'WebSQL is not supported in your browser'; 30 | } 31 | 32 | if (IdbFactory.supported) { 33 | var store = await IndexedDbStore.open('test', 'test'); 34 | runThrough(store, 'indexeddb'); 35 | } else { 36 | querySelector('#indexeddb').text = 37 | 'IndexedDB is not supported in your browser'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/local_storage_store.dart: -------------------------------------------------------------------------------- 1 | //Copyright 2012 Google 2 | // 3 | //Licensed under the Apache License, Version 2.0 (the "License"); 4 | //you may not use this file except in compliance with the License. 5 | //You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | //Unless required by applicable law or agreed to in writing, software 10 | //distributed under the License is distributed on an "AS IS" BASIS, 11 | //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | //See the License for the specific language governing permissions and 13 | //limitations under the License. 14 | 15 | part of lawndart; 16 | 17 | /** 18 | * Wraps the local storage API and exposes it as a [Store]. 19 | * Local storage is a synchronous API, and generally not recommended 20 | * unless all other storage mechanisms are unavailable. 21 | */ 22 | class LocalStorageStore extends _MapStore { 23 | LocalStorageStore._() : super._(); 24 | 25 | static Future open() async { 26 | var store = new LocalStorageStore._(); 27 | await store._open(); 28 | return store; 29 | } 30 | 31 | @override 32 | Map _generateMap() => window.localStorage; 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/_map_store.dart: -------------------------------------------------------------------------------- 1 | //Copyright 2012 Google 2 | // 3 | //Licensed under the Apache License, Version 2.0 (the "License"); 4 | //you may not use this file except in compliance with the License. 5 | //You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | //Unless required by applicable law or agreed to in writing, software 10 | //distributed under the License is distributed on an "AS IS" BASIS, 11 | //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | //See the License for the specific language governing permissions and 13 | //limitations under the License. 14 | 15 | part of lawndart; 16 | 17 | abstract class _MapStore extends Store { 18 | Map storage; 19 | 20 | _MapStore._() : super._(); 21 | 22 | @override 23 | Future _open() async { 24 | storage = _generateMap(); 25 | return true; 26 | } 27 | 28 | Map _generateMap(); 29 | 30 | @override 31 | Stream keys() async* { 32 | for (var k in storage.keys) { 33 | yield k; 34 | } 35 | } 36 | 37 | @override 38 | Future save(String obj, String key) async { 39 | storage[key] = obj; 40 | return key; 41 | } 42 | 43 | @override 44 | Future batch(Map objs) async { 45 | for (var key in objs.keys) { 46 | storage[key] = objs[key]; 47 | } 48 | return true; 49 | } 50 | 51 | @override 52 | Future getByKey(String key) async { 53 | return storage[key]; 54 | } 55 | 56 | @override 57 | Stream getByKeys(Iterable keys) async* { 58 | var values = keys.map((key) => storage[key]).where((v) => v != null); 59 | for (var v in values) { 60 | yield v; 61 | } 62 | } 63 | 64 | @override 65 | Future exists(String key) async { 66 | return storage.containsKey(key); 67 | } 68 | 69 | @override 70 | Stream all() async* { 71 | for (var v in storage.values) { 72 | yield v; 73 | } 74 | } 75 | 76 | @override 77 | Future removeByKey(String key) async { 78 | storage.remove(key); 79 | return true; 80 | } 81 | 82 | @override 83 | Future removeByKeys(Iterable keys) async { 84 | keys.forEach((key) => storage.remove(key)); 85 | return true; 86 | } 87 | 88 | Future nuke() async { 89 | storage.clear(); 90 | return true; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lawndart 2 | 3 | A unified, asynchronous, easy-to-use library for offline-enabled 4 | browser-based web apps. Kinda sorta a port of Lawnchair to Dart, 5 | but with Futures and Streams. 6 | 7 | Lawndart uses Futures to provide an asynchronous, yet consistent, 8 | interface to local storage, indexed db, and websql. This library is designed 9 | for simple key-value usage, and is not designed for complex transactional 10 | queries. This library prefers simplicity and uniformity over expressiveness. 11 | 12 | You can use this library to help deal with the wide array of client-side 13 | storage options. You should be able to write your code against the Lawndart 14 | interface and have it work across browsers that support at least one of the 15 | following: local storage, indexed db, and websql. 16 | 17 | # Example 18 | 19 | ```dart 20 | // Picks the best store available. 21 | var db = await Store.open('simple-run-through', 'test'); 22 | 23 | await db.open(); 24 | await db.nuke(); 25 | await db.save('world', 'hello'); 26 | await db.save('is fun', 'dart'); 27 | 28 | var value = await db.getByKey('hello'); 29 | 30 | querySelector('#text').text = value; 31 | ``` 32 | 33 | See the example/ directory for more sample code. 34 | 35 | # Choosing the best storage option 36 | 37 | This is now made easy for you. Simply create a new instance of Store: 38 | 39 | ```dart 40 | var store = await Store.open('dbName', 'storeName'); 41 | ``` 42 | 43 | The factory constructor will try IndexedDB, then WebSQL, and then finally 44 | local storage. Of course, you can perform your own logic to choose which 45 | option works for you. 46 | 47 | # API 48 | 49 | `Future Store.open()` 50 | Opens the database and makes it available for reading and writing. 51 | 52 | `Future nuke()` 53 | Wipes the database clean. All records are deleted. 54 | 55 | `Future save(value, key)` 56 | Stores a value accessible by a key. 57 | 58 | `Future getByKey(key)` 59 | Retrieves a value, given a key. 60 | 61 | `Stream keys()` 62 | Returns all keys. 63 | 64 | `Stream all()` 65 | Returns all values. 66 | 67 | `Future batch(map)` 68 | Stores all values and their keys. 69 | 70 | `Stream getByKeys(keys)` 71 | Returns all values, given keys. 72 | 73 | `Future exists(key)` 74 | Returns true if the key exists, or false. 75 | 76 | `Future removeByKey(key)` 77 | Removes the value for the key. 78 | 79 | `Future removeByKeys(keys)` 80 | Removes all values for the keys. 81 | 82 | 83 | # Usage 84 | 85 | Most methods return a Future, like `open` and `save`. 86 | Methods that would return many things, like `all`, return a Stream. 87 | 88 | # Supported storage mechanisms 89 | 90 | * Indexed DB - Great choice for modern browsers 91 | * WebSQL - Well supported in mobile WebKit browsers, not in Firefox 92 | * Local Storage - Only 5MB, slow, more-or-less universal 93 | * Memory - Good for testing 94 | 95 | You can consult [Can I Use?](http://caniuse.com) for a breakdown of browser 96 | support for the various storage technologies. 97 | 98 | # Install 99 | 100 | Lawndart is a pub package. To install it, and link it into your app, 101 | add lawndart to your pubspec.yaml. For example: 102 | 103 | ```yaml 104 | name: your_cool_app 105 | dependencies: 106 | lawndart: any 107 | ``` 108 | 109 | If you use Dart Editor, select your project from the Files view, then go 110 | to Tools, and run Pub Install. 111 | 112 | If you use the command line, ensure the Dart SDK is on your path, and 113 | the run: `pub install` 114 | 115 | # Support 116 | 117 | Lawndart is hosted at https://github.com/sethladd/lawndart 118 | 119 | You can file issues at https://github.com/sethladd/lawndart/issues 120 | 121 | API docs at http://sethladd.github.com/lawndart/ 122 | 123 | This library is open source, pull requests welcome! 124 | 125 | # Authors 126 | 127 | * Seth Ladd (sethladd@gmail.com) 128 | 129 | # License 130 | 131 | ```no-highlight 132 | Copyright 2015 Google 133 | 134 | Licensed under the Apache License, Version 2.0 (the "License"); 135 | you may not use this file except in compliance with the License. 136 | You may obtain a copy of the License at 137 | 138 | http://www.apache.org/licenses/LICENSE-2.0 139 | 140 | Unless required by applicable law or agreed to in writing, software 141 | distributed under the License is distributed on an "AS IS" BASIS, 142 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 143 | See the License for the specific language governing permissions and 144 | limitations under the License. 145 | ``` 146 | -------------------------------------------------------------------------------- /lib/src/indexeddb_store.dart: -------------------------------------------------------------------------------- 1 | //Copyright 2012 Google 2 | // 3 | //Licensed under the Apache License, Version 2.0 (the "License"); 4 | //you may not use this file except in compliance with the License. 5 | //You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | //Unless required by applicable law or agreed to in writing, software 10 | //distributed under the License is distributed on an "AS IS" BASIS, 11 | //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | //See the License for the specific language governing permissions and 13 | //limitations under the License. 14 | 15 | part of lawndart; 16 | 17 | /** 18 | * Wraps the IndexedDB API and exposes it as a [Store]. 19 | * IndexedDB is generally the preferred API if it is available. 20 | */ 21 | class IndexedDbStore extends Store { 22 | static Map _databases = new Map(); 23 | 24 | final String dbName; 25 | final String storeName; 26 | 27 | IndexedDbStore._(this.dbName, this.storeName) : super._(); 28 | 29 | static Future open(String dbName, String storeName) async { 30 | var store = new IndexedDbStore._(dbName, storeName); 31 | await store._open(); 32 | return store; 33 | } 34 | 35 | /// Returns true if IndexedDB is supported on this platform. 36 | static bool get supported => idb.IdbFactory.supported; 37 | 38 | Future _open() async { 39 | if (!supported) { 40 | throw new UnsupportedError('IndexedDB is not supported on this platform'); 41 | } 42 | 43 | if (_db != null) { 44 | _db.close(); 45 | } 46 | 47 | var db = await window.indexedDB.open(dbName); 48 | 49 | //print("Newly opened db $dbName has version ${db.version} and stores ${db.objectStoreNames}"); 50 | if (!db.objectStoreNames.contains(storeName)) { 51 | db.close(); 52 | //print('Attempting upgrading $storeName from ${db.version}'); 53 | db = await window.indexedDB.open(dbName, version: db.version + 1, 54 | onUpgradeNeeded: (e) { 55 | //print('Upgrading db $dbName to ${db.version + 1}'); 56 | idb.Database d = e.target.result; 57 | d.createObjectStore(storeName); 58 | }); 59 | } 60 | 61 | _databases[dbName] = db; 62 | return true; 63 | } 64 | 65 | idb.Database get _db => _databases[dbName]; 66 | 67 | @override 68 | Future removeByKey(String key) { 69 | return _runInTxn((store) => store.delete(key)); 70 | } 71 | 72 | @override 73 | Future save(String obj, String key) { 74 | return _runInTxn( 75 | (store) async => (await store.put(obj, key)) as String); 76 | } 77 | 78 | @override 79 | Future getByKey(String key) { 80 | return _runInTxn( 81 | (store) async => (await store.getObject(key) as String), 'readonly'); 82 | } 83 | 84 | @override 85 | Future nuke() { 86 | return _runInTxn((store) => store.clear()); 87 | } 88 | 89 | Future _runInTxn(Future requestCommand(idb.ObjectStore store), 90 | [String txnMode = 'readwrite']) async { 91 | var trans = _db.transaction(storeName, txnMode); 92 | var store = trans.objectStore(storeName); 93 | var result = await requestCommand(store); 94 | await trans.completed; 95 | return result; 96 | } 97 | 98 | Stream _doGetAll(String onCursor(idb.CursorWithValue cursor)) async* { 99 | var trans = _db.transaction(storeName, 'readonly'); 100 | var store = trans.objectStore(storeName); 101 | await for (var cursor in store.openCursor(autoAdvance: true)) { 102 | yield onCursor(cursor); 103 | } 104 | } 105 | 106 | @override 107 | Stream all() { 108 | return _doGetAll((idb.CursorWithValue cursor) => cursor.value); 109 | } 110 | 111 | @override 112 | Future batch(Map objs) { 113 | return _runInTxn((store) { 114 | objs.forEach((k, v) { 115 | store.put(v, k); 116 | }); 117 | }); 118 | } 119 | 120 | @override 121 | Stream getByKeys(Iterable keys) async* { 122 | for (var key in keys) { 123 | var v = await getByKey(key); 124 | if (v != null) yield v; 125 | } 126 | } 127 | 128 | @override 129 | Future removeByKeys(Iterable keys) { 130 | return _runInTxn((store) { 131 | for (var key in keys) { 132 | store.delete(key); 133 | } 134 | }); 135 | } 136 | 137 | @override 138 | Future exists(String key) async { 139 | var value = await getByKey(key); 140 | return value != null; 141 | } 142 | 143 | @override 144 | Stream keys() { 145 | return _doGetAll((idb.CursorWithValue cursor) => cursor.key); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/lawndart.dart: -------------------------------------------------------------------------------- 1 | //Copyright 2012 Google 2 | // 3 | //Licensed under the Apache License, Version 2.0 (the "License"); 4 | //you may not use this file except in compliance with the License. 5 | //You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | //Unless required by applicable law or agreed to in writing, software 10 | //distributed under the License is distributed on an "AS IS" BASIS, 11 | //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | //See the License for the specific language governing permissions and 13 | //limitations under the License. 14 | 15 | /** 16 | A unified, asynchronous, easy-to-use library for offline-enabled 17 | browser-based web apps. Kinda sorta a port of Lawnchair to Dart, 18 | but with Futures and Streams. 19 | 20 | Lawndart uses Futures to provide an asynchronous, yet consistent, 21 | interface to local storage, indexed db, and websql. This library is designed 22 | for simple key-value usage, and is not designed for complex transactional 23 | queries. This library prefers simplicity and uniformity over expressiveness. 24 | 25 | You can use this library to help deal with the wide array of client-side 26 | storage options. You should be able to write your code against the Lawndart 27 | interface and have it work across browsers that support at least one of the 28 | following: local storage, indexed db, and websql. 29 | 30 | # Example 31 | 32 | var db = new IndexedDbStore('simple-run-through', 'test'); 33 | db.open() 34 | .then((_) => db.nuke()) 35 | .then((_) => db.save("world", "hello")) 36 | .then((_) => db.save("is fun", "dart")) 37 | .then((_) => db.getByKey("hello")) 38 | .then((value) => query('#text').text = value); 39 | 40 | See the `example/` directory for more sample code. 41 | 42 | */ 43 | library lawndart; 44 | 45 | import 'dart:html'; 46 | import 'dart:indexed_db' as idb; 47 | import 'dart:web_sql'; 48 | import 'dart:async'; 49 | 50 | part 'src/indexeddb_store.dart'; 51 | part 'src/_map_store.dart'; 52 | part 'src/memory_store.dart'; 53 | part 'src/local_storage_store.dart'; 54 | part 'src/websql_store.dart'; 55 | 56 | /** 57 | * Represents a Store that can hold key/value pairs. No order 58 | * is guaranteed for either keys or values. 59 | */ 60 | abstract class Store { 61 | // For subclasses 62 | Store._(); 63 | 64 | /** 65 | * Finds the best implementation. In order: IndexedDB, WebSQL, LocalStorage. 66 | */ 67 | static Future open(String dbName, String storeName, 68 | [Map options]) async { 69 | Store store; 70 | if (IndexedDbStore.supported) { 71 | store = new IndexedDbStore._(dbName, storeName); 72 | } else if (WebSqlStore.supported) { 73 | if (options != null && options['estimatedSize']) { 74 | store = new WebSqlStore._(dbName, storeName, 75 | estimatedSize: options['estimatedSize']); 76 | } else { 77 | store = new WebSqlStore._(dbName, storeName); 78 | } 79 | } else { 80 | store = new LocalStorageStore._(); 81 | } 82 | 83 | await store._open(); 84 | 85 | return store; 86 | } 87 | 88 | /// Opens and initializes the database. 89 | Future _open(); 90 | 91 | /// Returns all the keys as a stream. No order is guaranteed. 92 | Stream keys(); 93 | 94 | /// Stores an [obj] accessible by [key]. 95 | /// The returned Future completes with the key when the objects 96 | /// is saved in the store. 97 | Future save(String obj, String key); 98 | 99 | /// Stores all objects by their keys. This should happen in a single 100 | /// transaction if the underlying store supports it. 101 | /// The returned Future completes when all objects have been added 102 | /// to the store. 103 | Future batch(Map objectsByKey); 104 | 105 | /// Returns a Future that completes with the value for a key, 106 | /// or null if the key does not exist. 107 | Future getByKey(String key); 108 | 109 | /// Returns a Stream of all values for the keys. 110 | /// If a particular key is not found, 111 | /// no value will be returned, not even null. 112 | Stream getByKeys(Iterable keys); 113 | 114 | /// Returns a Future that completes with true if the key exists, or false. 115 | Future exists(String key); 116 | 117 | /// Returns a Stream of all values in no particular order. 118 | Stream all(); 119 | 120 | /// Returns a Future that completes when the key's value is removed. 121 | Future removeByKey(String key); 122 | 123 | /// Returns a Future that completes when all the keys' values are removed. 124 | Future removeByKeys(Iterable keys); 125 | 126 | /// Returns a Future that completes when all values and keys 127 | /// are removed. 128 | Future nuke(); 129 | } 130 | -------------------------------------------------------------------------------- /test/lawndart_test.dart: -------------------------------------------------------------------------------- 1 | //Copyright 2014 Google 2 | // 3 | //Licensed under the Apache License, Version 2.0 (the "License"); 4 | //you may not use this file except in compliance with the License. 5 | //You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | //Unless required by applicable law or agreed to in writing, software 10 | //distributed under the License is distributed on an "AS IS" BASIS, 11 | //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | //See the License for the specific language governing permissions and 13 | //limitations under the License. 14 | 15 | library store_tests; 16 | 17 | import 'dart:async'; 18 | import 'dart:indexed_db'; 19 | import 'dart:web_sql'; 20 | import 'package:test/test.dart'; 21 | import 'package:lawndart/lawndart.dart'; 22 | 23 | typedef Future StoreGenerator(); 24 | 25 | void run(StoreGenerator generator) { 26 | Store store; 27 | 28 | group('with no values', () { 29 | setUp(() async { 30 | store = await generator(); 31 | await store.nuke(); 32 | }); 33 | 34 | test('keys is empty', () async { 35 | var keys = await store.keys().toList(); 36 | expect(keys, hasLength(0)); 37 | }); 38 | 39 | test('get by key return null', () { 40 | Future future = store.getByKey("foo"); 41 | expect(future, completion(null)); 42 | }); 43 | 44 | test('get by keys return empty collection', () async { 45 | var list = await store.getByKeys(["foo"]).toList(); 46 | expect(list, hasLength(0)); 47 | }); 48 | 49 | test('save completes', () { 50 | Future future = store.save("value", "key"); 51 | expect(future, completion("key")); 52 | }); 53 | 54 | test('exists returns false', () { 55 | Future future = store.exists("foo"); 56 | expect(future, completion(false)); 57 | }); 58 | 59 | test('all is empty', () { 60 | Future future = store.all().toList(); 61 | expect(future, completion(hasLength(0))); 62 | }); 63 | 64 | test('remove by key completes', () { 65 | Future future = store.removeByKey("foo"); 66 | expect(future, completes); 67 | }); 68 | 69 | test('remove by keys completes', () { 70 | Future future = store.removeByKeys(["foo"]); 71 | expect(future, completes); 72 | }); 73 | 74 | test('nuke completes', () { 75 | Future future = store.nuke(); 76 | expect(future, completes); 77 | }); 78 | 79 | test('batch completes', () { 80 | Future future = store.batch({'foo': 'bar'}); 81 | expect(future, completes); 82 | }); 83 | }); 84 | 85 | group('with a few values', () { 86 | setUp(() async { 87 | // ensure it's clear for each test, see http://dartbug.com/8157 88 | store = await generator(); 89 | 90 | await store.nuke(); 91 | await store.save("world", "hello"); 92 | await store.save("is fun", "dart"); 93 | }); 94 | 95 | test('keys has them', () { 96 | Future future = store.keys().toList(); 97 | future.then((Iterable keys) { 98 | expect(keys, hasLength(2)); 99 | expect(keys, contains("hello")); 100 | expect(keys, contains("dart")); 101 | }); 102 | expect(future, completes); 103 | }); 104 | 105 | test('get by key', () { 106 | Future future = store.getByKey("hello"); 107 | future.then((value) { 108 | expect(value, "world"); 109 | }); 110 | expect(future, completes); 111 | }); 112 | 113 | test('get by keys', () { 114 | Future future = store.getByKeys(["hello", "dart"]).toList(); 115 | future.then((values) { 116 | expect(values, hasLength(2)); 117 | expect(values.contains("world"), true); 118 | expect(values.contains("is fun"), true); 119 | }); 120 | expect(future, completes); 121 | }); 122 | 123 | test('exists is true', () { 124 | Future future = store.exists("hello"); 125 | future.then((exists) { 126 | expect(exists, true); 127 | }); 128 | expect(future, completes); 129 | }); 130 | 131 | test('all has everything', () { 132 | Future future = store.all().toList(); 133 | future.then((all) { 134 | expect(all, hasLength(2)); 135 | expect(all.contains("world"), true); 136 | expect(all.contains("is fun"), true); 137 | }); 138 | expect(future, completes); 139 | }); 140 | 141 | test('remove by key', () { 142 | Future future = 143 | store.removeByKey("hello").then((_) => store.all().toList()); 144 | future.then((remaining) { 145 | expect(remaining, hasLength(1)); 146 | expect(remaining.contains("world"), false); 147 | expect(remaining.contains("is fun"), true); 148 | }); 149 | expect(future, completes); 150 | }); 151 | }); 152 | } 153 | 154 | void main() { 155 | group('memory', () { 156 | run(() => MemoryStore.open()); 157 | }); 158 | 159 | group('local storage', () { 160 | run(() => LocalStorageStore.open()); 161 | }); 162 | 163 | if (SqlDatabase.supported) { 164 | group('websql', () { 165 | run(() => WebSqlStore.open('test', 'test')); 166 | }); 167 | } 168 | 169 | if (IdbFactory.supported) { 170 | group('indexed db store0', () { 171 | run(() => IndexedDbStore.open("test-db", "test-store0")); 172 | }); 173 | group('indexed db store1', () { 174 | run(() => IndexedDbStore.open("test-db", "test-store1")); 175 | }); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /lib/src/websql_store.dart: -------------------------------------------------------------------------------- 1 | //Copyright 2012 Google 2 | // 3 | //Licensed under the Apache License, Version 2.0 (the "License"); 4 | //you may not use this file except in compliance with the License. 5 | //You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | //Unless required by applicable law or agreed to in writing, software 10 | //distributed under the License is distributed on an "AS IS" BASIS, 11 | //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | //See the License for the specific language governing permissions and 13 | //limitations under the License. 14 | 15 | part of lawndart; 16 | 17 | /** 18 | * Wraps the WebSQL API and exposes it as a [Store]. 19 | * WebSQL is a transactional database. 20 | */ 21 | class WebSqlStore extends Store { 22 | static const String VERSION = "1"; 23 | static const int INITIAL_SIZE = 4 * 1024 * 1024; 24 | 25 | String dbName; 26 | String storeName; 27 | int estimatedSize; 28 | SqlDatabase _db; 29 | 30 | WebSqlStore._(this.dbName, this.storeName, {this.estimatedSize: INITIAL_SIZE}) 31 | : super._(); 32 | 33 | static Future open(String dbName, String storeName, 34 | {int estimatedSize: INITIAL_SIZE}) async { 35 | final store = 36 | new WebSqlStore._(dbName, storeName, estimatedSize: estimatedSize); 37 | await store._open(); 38 | return store; 39 | } 40 | 41 | /// Returns true if WebSQL is supported on this platform. 42 | static bool get supported => SqlDatabase.supported; 43 | 44 | @override 45 | Future _open() async { 46 | if (!supported) { 47 | throw new UnsupportedError('WebSQL is not supported on this platform'); 48 | } 49 | _db = window.openDatabase(dbName, VERSION, dbName, estimatedSize); 50 | await _initDb(); 51 | return true; 52 | } 53 | 54 | Future _initDb() { 55 | final sql = 56 | 'CREATE TABLE IF NOT EXISTS $storeName (id NVARCHAR(32) UNIQUE PRIMARY KEY, value TEXT)'; 57 | return _runInTxn((txn, completer) { 58 | txn.executeSql(sql, []); 59 | }); 60 | } 61 | 62 | @override 63 | Stream keys() { 64 | final sql = 'SELECT id FROM $storeName'; 65 | return _runInTxnWithResults((txn, controller) async { 66 | final resultSet = await txn.executeSql(sql, []); 67 | for (var i = 0; i < resultSet.rows.length; ++i) { 68 | final row = resultSet.rows.item(i); 69 | controller.add(row['id']); 70 | } 71 | }); 72 | } 73 | 74 | @override 75 | Future save(String obj, String key) { 76 | final upsertSql = 77 | 'INSERT OR REPLACE INTO $storeName (id, value) VALUES (?, ?)'; 78 | return _runInTxn((txn, completer) async { 79 | await txn.executeSql(upsertSql, [key, obj]); 80 | completer.complete(key); 81 | }); 82 | } 83 | 84 | @override 85 | Future exists(String key) async { 86 | final v = await getByKey(key); 87 | return v != null; 88 | } 89 | 90 | @override 91 | Future getByKey(String key) { 92 | final completer = new Completer(); 93 | final sql = 'SELECT value FROM $storeName WHERE id = ?'; 94 | 95 | _db.readTransaction((txn) async { 96 | final resultSet = await txn.executeSql(sql, [key]); 97 | if (resultSet.rows.isEmpty) { 98 | completer.complete(null); 99 | } else { 100 | final row = resultSet.rows.item(0); 101 | completer.complete(row['value']); 102 | } 103 | }, (error) => completer.completeError(error)); 104 | 105 | return completer.future; 106 | } 107 | 108 | @override 109 | Future removeByKey(String key) { 110 | final sql = 'DELETE FROM $storeName WHERE id = ?'; 111 | 112 | return _runInTxn((txn, completer) { 113 | txn.executeSql(sql, [key]); 114 | }); 115 | } 116 | 117 | @override 118 | Future nuke() { 119 | final sql = 'DELETE FROM $storeName'; 120 | return _runInTxn((txn, completer) { 121 | txn.executeSql(sql, []); 122 | }); 123 | } 124 | 125 | @override 126 | Stream all() { 127 | final sql = 'SELECT id,value FROM $storeName'; 128 | 129 | return _runInTxnWithResults((txn, controller) async { 130 | final resultSet = await txn.executeSql(sql, []); 131 | for (var i = 0; i < resultSet.rows.length; ++i) { 132 | final row = resultSet.rows.item(i); 133 | controller.add(row['value']); 134 | } 135 | }); 136 | } 137 | 138 | @override 139 | Future batch(Map objs) { 140 | final upsertSql = 141 | 'INSERT OR REPLACE INTO $storeName (id, value) VALUES (?, ?)'; 142 | 143 | return _runInTxn((txn, completer) { 144 | objs.forEach((key, value) { 145 | txn.executeSql(upsertSql, [key, value]); 146 | }); 147 | }); 148 | } 149 | 150 | @override 151 | Stream getByKeys(Iterable _keys) { 152 | final sql = 'SELECT value FROM $storeName WHERE id = ?'; 153 | return _runInTxnWithResults((txn, controller) { 154 | _keys.forEach((key) async { 155 | final resultSet = await txn.executeSql(sql, [key]); 156 | if (resultSet.rows.isNotEmpty) { 157 | controller.add(resultSet.rows.item(0)['value']); 158 | } 159 | }); 160 | }); 161 | } 162 | 163 | @override 164 | Future removeByKeys(Iterable _keys) { 165 | final sql = 'DELETE FROM $storeName WHERE id = ?'; 166 | return _runInTxn((txn, completer) { 167 | _keys.forEach((key) { 168 | txn.executeSql(sql, [key]); 169 | }); 170 | }); 171 | } 172 | 173 | Future _runInTxn( 174 | Future callback(SqlTransaction txn, Completer completer)) { 175 | final completer = new Completer(); 176 | 177 | _db.transaction((txn) => callback(txn, completer), 178 | (error) => completer.completeError(error), () { 179 | if (!completer.isCompleted) { 180 | completer.complete(); 181 | } 182 | }); 183 | 184 | return completer.future; 185 | } 186 | 187 | Stream _runInTxnWithResults( 188 | Future callback(SqlTransaction txn, StreamController controller)) { 189 | final controller = new StreamController(); 190 | 191 | _db.transaction((txn) => callback(txn, controller), (error) { 192 | controller.addError(error); 193 | controller.close(); 194 | }, () => controller.close()); 195 | 196 | return controller.stream; 197 | } 198 | } 199 | --------------------------------------------------------------------------------