-
-
Notifications
You must be signed in to change notification settings - Fork 641
Simplify with yield
If you target modern browsers like Chrome, Opera, Firefox or Edge, Android WebView or Chrome for Mobile, or if you transpile your code from ES6 to ES5, there's no reason to not start consuming promises using the yield keyword.
The principle is simple:
- Use the latest version of Dexie (1.3 or later)
- Use spawn() or async() to enable a synchronous-like programming style.
- Each method that returns a Promise can be awaited using yield. Exactly the same way as ES7's async/await is going to work.
import Dexie from 'dexie';
const async = Dexie.async;
const spawn = Dexie.spawn;
export var db = new Dexie('testdb');
db.version(1).stores({
people: '++id,name,age',
assets: '++id,ownerId,assetName'
});
spawn(function*() {
//
// Write your synchronous-like code within a spawn() block
//
// Add a person:
let id = yield db.people.add({name: 'Foo', age: 34});
// Add two assets that links to the newly added person:
db.assets.bulkAdd([
{assetName: "car", ownerId: id},
{assetName: "house", ownerId: id}
]);
// Now, list all people and their assets:
let people = yield db.people.toArray();
for (let i=0; i<people.length; ++i) {
let assets = yield db.assets
.where('ownerId').equals(people[i].id)
.toArray();
console.log(`${people[i].name}'s assets: ${assets.map(a => a.assetName).join(',')}`);
}
}).then(function() {
//
// spawn() returns a promise that completes when all is done.
//
console.log("Complete");
}).catch(function(e) {
//
// If any error occur in DB or plain exceptions, you
// may catch them here.
//
console.error("Failed: " + e);
});
import Dexie from 'dexie';
const async = Dexie.async;
const spawn = Dexie.spawn;
var db = new Dexie("myDB");
db.version(1).stores({
friends: '++id, name, age'
});
db.open();
db.transaction('rw', db.friends, function*(){
var friendId = yield db.friends.add({name: "Foo", age: 42});
console.log("Got id: " + friendId);
console.log("These are my friends: " + JSON.stringify(yield db.friends.toArray()));
yield db.friends.delete(friendId);
console.log ("Friend successfully deleted.");
}).catch(e => alert ("Oops: " + e));
Marking a generator function as async() will make yield statements behave like ES7 await. This is not needed in transaction callbacks (as above sample shows) but can be used whenever you need to do several transactions, or not use transactions at all.
import Dexie from 'dexie';
const async = Dexie.async;
const spawn = Dexie.spawn;
...
var listFriends = async(function*() {
var friends = yield db.friends.toArray();
return friends;
});
listFriends()
.then(friends => console.log(JSON.stringify(friends)))
.catch(e => console.error (e.stack));
Another style is using spawn() instead of async(). Then you don't need to store your async functions in vars.
import Dexie from 'dexie';
const async = Dexie.async;
const spawn = Dexie.spawn;
...
function* listFriends () {
var friends = yield db.friends.toArray();
return friends;
};
spawn(listFriends)
.then(friends => console.log(JSON.stringify(friends)))
.catch(e => console.error (e.stack));
There are two possible of structuring your code with sub functions.
- Method 1: Declare each function with Dexie.async(). Declaring each function with async is most declarative but requires var declaration with function* expressions instead of function* statements.
- Method 2: Just declare as function*. This method gives cleaner code, but it requires the jsdocs to clarify how they are supposed to be consumed. Generator functions are not always used for emulating async/await, so it cannot be assumed that they should be called via spawn() or yield*.
NOTE: Using ES5 style vars to make the samples work in todays browsers (March 2016).
var async = Dexie.async;
var incrementAge = async(function* (friendId) {
yield db.friends
.where('id').equals(friendId)
.modify(friend => ++friend.age);
});
var listFriends = async(function* () {
var friends = yield db.friends.toArray();
return friends;
});
var birthdays = async(function* () {
var friends = yield listFriends();
for (var i=0; i<friends.length; ++) {
yield incrementAge(friends[i].id);
}
});
Rule of thumb is:
- Calling a generator function will give you an Iterable, not a Promise.
- When awaiting a Promise (for example returned from Dexie API), use
yield
. - When awaiting an Iterable (the result from calling a function*), use
yield*
function* incrementAge(friendId) {
yield db.friends
.where('id').equals(friendId)
.modify(friend => ++friend.age);
}
function* listFriends () {
var friends = yield db.friends.toArray();
return friends;
}
function* birthdays() {
var friends = yield* listFriends();
for (var i=0; i<friends.length; ++) {
yield* incrementAge(friend[i].id);
}
}
Dexie.spawn(birthdays).catch(e => console.error (e));
Table below shows how this maps to ES7 async / await.
+--------------------------------+--------------------------+
| Using function*() and yield | Using async / await |
+--------------------------------+--------------------------+
Declare async function | Dexie.async(function* () {}); | async function() {} |
+--------------------------------+--------------------------+
Declare+execute function | Dexie.spawn(function* () {}); | (async function() {})() |
+--------------------------------+--------------------------+
Await a Promise | yield p; | await p; |
+--------------------------------+--------------------------+
Declare Promise Generator| function* f (){} | N/A |
+-----------------------------------------------------------+
Await Promise Generator | yield* f(); | N/A |
+-----------------------------------------------------------+
You can also find the spawn() and async() helpers other libs like Q, Task.js etc. The reason why we need 'yet another' one, is because those will all return their specific types of Promises, which in some browsers are incompatible with indexedDB transactions. That's also the main reason Dexie needs its own Promise implementation. Furthermore, Dexie Promises are capable of maintaining Promise-Specific Data (analogous to Thread-specific data) and utilize that for maintaining transaction scopes and reentrant transaction locks.
However, the Dexie versions of async() and spawn() will adapt to any promise implementation so you can use it to consume other promises as well if you like.
Dexie.js - minimalistic and bullet proof indexedDB library