-
-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
experimenting with new pledge replacement for promises
- Loading branch information
Showing
8 changed files
with
718 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
/* eslint-env browser */ | ||
|
||
/** | ||
* Helpers to work with IndexedDB. | ||
* This is an experimental implementation using Pledge instead of Promise. | ||
* | ||
* @experimental | ||
* | ||
* @module indexeddbv2 | ||
*/ | ||
|
||
import * as pledge from './pledge.js' | ||
|
||
/* c8 ignore start */ | ||
|
||
/** | ||
* IDB Request to Pledge transformer | ||
* | ||
* @param {pledge.PledgeInstance<any>} p | ||
* @param {IDBRequest} request | ||
*/ | ||
export const bindPledge = (p, request) => { | ||
// @ts-ignore | ||
request.onerror = event => p.cancel(event.target.error) | ||
// @ts-ignore | ||
request.onsuccess = event => p.resolve(event.target.result) | ||
} | ||
|
||
/** | ||
* @param {string} name | ||
* @param {function(IDBDatabase):any} initDB Called when the database is first created | ||
* @return {pledge.PledgeInstance<IDBDatabase>} | ||
*/ | ||
export const openDB = (name, initDB) => { | ||
/** | ||
* @type {pledge.PledgeInstance<IDBDatabase>} | ||
*/ | ||
const p = pledge.create() | ||
const request = indexedDB.open(name) | ||
/** | ||
* @param {any} event | ||
*/ | ||
request.onupgradeneeded = event => initDB(event.target.result) | ||
/** | ||
* @param {any} event | ||
*/ | ||
request.onerror = event => p.cancel(event.target.error) | ||
/** | ||
* @param {any} event | ||
*/ | ||
request.onsuccess = event => { | ||
/** | ||
* @type {IDBDatabase} | ||
*/ | ||
const db = event.target.result | ||
db.onversionchange = () => { db.close() } | ||
p.resolve(db) | ||
} | ||
return p | ||
} | ||
|
||
/** | ||
* @param {pledge.Pledge<string>} name | ||
* @return {pledge.PledgeInstance<void>} | ||
*/ | ||
export const deleteDB = name => pledge.createWithDependencies((p, name) => bindPledge(p, indexedDB.deleteDatabase(name)), name) | ||
|
||
/** | ||
* @param {IDBDatabase} db | ||
* @param {Array<Array<string>|Array<string|IDBObjectStoreParameters|undefined>>} definitions | ||
*/ | ||
export const createStores = (db, definitions) => definitions.forEach(d => | ||
// @ts-ignore | ||
db.createObjectStore.apply(db, d) | ||
) | ||
|
||
/** | ||
* @param {pledge.Pledge<IDBDatabase>} db | ||
* @param {pledge.Pledge<Array<string>>} stores | ||
* @param {"readwrite"|"readonly"} [access] | ||
* @return {pledge.Pledge<Array<IDBObjectStore>>} | ||
*/ | ||
export const transact = (db, stores, access = 'readwrite') => pledge.createWithDependencies((p, db, stores) => { | ||
const transaction = db.transaction(stores, access) | ||
p.resolve(stores.map(store => getStore(transaction, store))) | ||
}, db, stores) | ||
|
||
/** | ||
* @param {IDBObjectStore} store | ||
* @param {pledge.Pledge<IDBKeyRange|undefined>} [range] | ||
* @return {pledge.PledgeInstance<number>} | ||
*/ | ||
export const count = (store, range) => pledge.createWithDependencies((p, store, range) => bindPledge(p, store.count(range)), store, range) | ||
|
||
/** | ||
* @param {pledge.Pledge<IDBObjectStore>} store | ||
* @param {pledge.Pledge<String | number | ArrayBuffer | Date | Array<any>>} key | ||
* @return {pledge.PledgeInstance<String | number | ArrayBuffer | Date | Array<any>>} | ||
*/ | ||
export const get = (store, key) => pledge.createWithDependencies((p, store, key) => bindPledge(p, store.get(key)), store, key) | ||
|
||
/** | ||
* @param {pledge.Pledge<IDBObjectStore>} store | ||
* @param {String | number | ArrayBuffer | Date | IDBKeyRange | Array<any> } key | ||
*/ | ||
export const del = (store, key) => pledge.createWithDependencies((p, store, key) => bindPledge(p, store.delete(key)), store, key) | ||
|
||
/** | ||
* @param {pledge.Pledge<IDBObjectStore>} store | ||
* @param {String | number | ArrayBuffer | Date | boolean} item | ||
* @param {String | number | ArrayBuffer | Date | Array<any>} [key] | ||
*/ | ||
export const put = (store, item, key) => pledge.createWithDependencies((p, store, item, key) => bindPledge(p, store.put(item, key)), store, item, key) | ||
|
||
/** | ||
* @param {pledge.Pledge<IDBObjectStore>} store | ||
* @param {String | number | ArrayBuffer | Date | boolean} item | ||
* @param {String | number | ArrayBuffer | Date | Array<any>} key | ||
* @return {pledge.PledgeInstance<any>} | ||
*/ | ||
export const add = (store, item, key) => pledge.createWithDependencies((p, store, item, key) => bindPledge(p, store.add(item, key)), store, item, key) | ||
|
||
/** | ||
* @param {pledge.Pledge<IDBObjectStore>} store | ||
* @param {String | number | ArrayBuffer | Date} item | ||
* @return {pledge.PledgeInstance<number>} Returns the generated key | ||
*/ | ||
export const addAutoKey = (store, item) => pledge.createWithDependencies((p, store, item) => bindPledge(p, store.add(item)), store, item) | ||
|
||
/** | ||
* @param {pledge.Pledge<IDBObjectStore>} store | ||
* @param {IDBKeyRange} [range] | ||
* @param {number} [limit] | ||
* @return {pledge.PledgeInstance<Array<any>>} | ||
*/ | ||
export const getAll = (store, range, limit) => pledge.createWithDependencies((p, store, range, limit) => bindPledge(p, store.getAll(range, limit)), store, range, limit) | ||
|
||
/** | ||
* @param {pledge.Pledge<IDBObjectStore>} store | ||
* @param {IDBKeyRange} [range] | ||
* @param {number} [limit] | ||
* @return {pledge.PledgeInstance<Array<any>>} | ||
*/ | ||
export const getAllKeys = (store, range, limit) => pledge.createWithDependencies((p, store, range, limit) => bindPledge(p, store.getAllKeys(range, limit)), store, range, limit) | ||
|
||
/** | ||
* @param {IDBObjectStore} store | ||
* @param {IDBKeyRange|null} query | ||
* @param {'next'|'prev'|'nextunique'|'prevunique'} direction | ||
* @return {pledge.PledgeInstance<any>} | ||
*/ | ||
export const queryFirst = (store, query, direction) => { | ||
/** | ||
* @type {any} | ||
*/ | ||
let first = null | ||
return iterateKeys(store, query, key => { | ||
first = key | ||
return false | ||
}, direction).map(() => first) | ||
} | ||
|
||
/** | ||
* @param {IDBObjectStore} store | ||
* @param {IDBKeyRange?} [range] | ||
* @return {pledge.PledgeInstance<any>} | ||
*/ | ||
export const getLastKey = (store, range = null) => queryFirst(store, range, 'prev') | ||
|
||
/** | ||
* @param {IDBObjectStore} store | ||
* @param {IDBKeyRange?} [range] | ||
* @return {pledge.PledgeInstance<any>} | ||
*/ | ||
export const getFirstKey = (store, range = null) => queryFirst(store, range, 'next') | ||
|
||
/** | ||
* @typedef KeyValuePair | ||
* @type {Object} | ||
* @property {any} k key | ||
* @property {any} v Value | ||
*/ | ||
|
||
/** | ||
* @param {pledge.Pledge<IDBObjectStore>} store | ||
* @param {pledge.Pledge<IDBKeyRange|undefined>} [range] | ||
* @param {pledge.Pledge<number|undefined>} [limit] | ||
* @return {pledge.PledgeInstance<Array<KeyValuePair>>} | ||
*/ | ||
export const getAllKeysValues = (store, range, limit) => pledge.createWithDependencies((p, store, range, limit) => { | ||
pledge.all([getAllKeys(store, range, limit), getAll(store, range, limit)]).map(([ks, vs]) => ks.map((k, i) => ({ k, v: vs[i] }))).whenResolved(p.resolve.bind(p)) | ||
}, store, range, limit) | ||
|
||
/** | ||
* @param {pledge.PledgeInstance<void>} p | ||
* @param {any} request | ||
* @param {function(IDBCursorWithValue):void|boolean|Promise<void|boolean>} f | ||
*/ | ||
const iterateOnRequest = (p, request, f) => { | ||
request.onerror = p.cancel.bind(p) | ||
/** | ||
* @param {any} event | ||
*/ | ||
request.onsuccess = async event => { | ||
const cursor = event.target.result | ||
if (cursor === null || (await f(cursor)) === false) { | ||
p.resolve(undefined) | ||
return | ||
} | ||
cursor.continue() | ||
} | ||
} | ||
|
||
/** | ||
* Iterate on keys and values | ||
* @param {pledge.Pledge<IDBObjectStore>} store | ||
* @param {pledge.Pledge<IDBKeyRange|null>} keyrange | ||
* @param {function(any,any):void|boolean|Promise<void|boolean>} f Callback that receives (value, key) | ||
* @param {'next'|'prev'|'nextunique'|'prevunique'} direction | ||
*/ | ||
export const iterate = (store, keyrange, f, direction = 'next') => pledge.createWithDependencies((p, store, keyrange) => { | ||
iterateOnRequest(p, store.openCursor(keyrange, direction), cursor => f(cursor.value, cursor.key)) | ||
}, store, keyrange) | ||
|
||
/** | ||
* Iterate on the keys (no values) | ||
* | ||
* @param {pledge.Pledge<IDBObjectStore>} store | ||
* @param {pledge.Pledge<IDBKeyRange|null>} keyrange | ||
* @param {function(any):void|boolean|Promise<void|boolean>} f callback that receives the key | ||
* @param {'next'|'prev'|'nextunique'|'prevunique'} direction | ||
*/ | ||
export const iterateKeys = (store, keyrange, f, direction = 'next') => pledge.createWithDependencies((p, store, keyrange) => { | ||
iterateOnRequest(p, store.openKeyCursor(keyrange, direction), cursor => f(cursor.key)) | ||
}, store, keyrange) | ||
|
||
/** | ||
* Open store from transaction | ||
* @param {IDBTransaction} t | ||
* @param {String} store | ||
* @returns {IDBObjectStore} | ||
*/ | ||
export const getStore = (t, store) => t.objectStore(store) | ||
|
||
/** | ||
* @param {any} lower | ||
* @param {any} upper | ||
* @param {boolean} lowerOpen | ||
* @param {boolean} upperOpen | ||
*/ | ||
export const createIDBKeyRangeBound = (lower, upper, lowerOpen, upperOpen) => IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen) | ||
|
||
/** | ||
* @param {any} upper | ||
* @param {boolean} upperOpen | ||
*/ | ||
export const createIDBKeyRangeUpperBound = (upper, upperOpen) => IDBKeyRange.upperBound(upper, upperOpen) | ||
|
||
/** | ||
* @param {any} lower | ||
* @param {boolean} lowerOpen | ||
*/ | ||
export const createIDBKeyRangeLowerBound = (lower, lowerOpen) => IDBKeyRange.lowerBound(lower, lowerOpen) | ||
|
||
/* c8 ignore stop */ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import * as t from './testing.js' | ||
import * as idb from './indexeddbV2.js' | ||
import * as pledge from './pledge.js' | ||
import { isBrowser } from './environment.js' | ||
|
||
/* c8 ignore next */ | ||
/** | ||
* @param {IDBDatabase} db | ||
*/ | ||
const initTestDB = db => idb.createStores(db, [['test', { autoIncrement: true }]]) | ||
const testDBName = 'idb-test' | ||
|
||
/* c8 ignore next */ | ||
/** | ||
* @param {pledge.Pledge<IDBDatabase>} db | ||
*/ | ||
const createTransaction = db => pledge.createWithDependencies((p, db) => p.resolve(db.transaction(['test'], 'readwrite')), db) | ||
|
||
/* c8 ignore next */ | ||
/** | ||
* @param {pledge.Pledge<IDBTransaction>} t | ||
* @return {pledge.PledgeInstance<IDBObjectStore>} | ||
*/ | ||
const getStore = t => pledge.createWithDependencies((p, t) => p.resolve(idb.getStore(t, 'test')), t) | ||
|
||
/* c8 ignore next */ | ||
export const testRetrieveElements = async () => { | ||
t.skip(!isBrowser) | ||
t.describe('create, then iterate some keys') | ||
await idb.deleteDB(testDBName).promise() | ||
const db = idb.openDB(testDBName, initTestDB) | ||
const transaction = createTransaction(db) | ||
const store = getStore(transaction) | ||
await idb.put(store, 0, ['t', 1]).promise() | ||
await idb.put(store, 1, ['t', 2]).promise() | ||
const expectedKeys = [['t', 1], ['t', 2]] | ||
const expectedVals = [0, 1] | ||
const expectedKeysVals = [{ v: 0, k: ['t', 1] }, { v: 1, k: ['t', 2] }] | ||
t.describe('idb.getAll') | ||
const valsGetAll = await idb.getAll(store).promise() | ||
t.compare(valsGetAll, expectedVals) | ||
t.describe('idb.getAllKeys') | ||
const valsGetAllKeys = await idb.getAllKeys(store).promise() | ||
t.compare(valsGetAllKeys, expectedKeys) | ||
t.describe('idb.getAllKeysVals') | ||
const valsGetAllKeysVals = await idb.getAllKeysValues(store).promise() | ||
t.compare(valsGetAllKeysVals, expectedKeysVals) | ||
|
||
/** | ||
* @param {string} desc | ||
* @param {IDBKeyRange?} keyrange | ||
*/ | ||
const iterateTests = async (desc, keyrange) => { | ||
t.describe(`idb.iterate (${desc})`) | ||
/** | ||
* @type {Array<{v:any,k:any}>} | ||
*/ | ||
const valsIterate = [] | ||
await idb.iterate(store, keyrange, (v, k) => { | ||
valsIterate.push({ v, k }) | ||
}).promise() | ||
t.compare(valsIterate, expectedKeysVals) | ||
t.describe(`idb.iterateKeys (${desc})`) | ||
/** | ||
* @type {Array<any>} | ||
*/ | ||
const keysIterate = [] | ||
await idb.iterateKeys(store, keyrange, key => { | ||
keysIterate.push(key) | ||
}).promise() | ||
t.compare(keysIterate, expectedKeys) | ||
} | ||
await iterateTests('range=null', null) | ||
const range = idb.createIDBKeyRangeBound(['t', 1], ['t', 2], false, false) | ||
// adding more items that should not be touched by iteration with above range | ||
await idb.put(store, 2, ['t', 3]).promise() | ||
await idb.put(store, 2, ['t', 0]).promise() | ||
await iterateTests('range!=null', range) | ||
|
||
t.describe('idb.get') | ||
const getV = await idb.get(store, ['t', 1]).promise() | ||
t.assert(getV === 0) | ||
t.describe('idb.del') | ||
await idb.del(store, ['t', 0]).promise() | ||
const getVDel = await idb.get(store, ['t', 0]).promise() | ||
t.assert(getVDel === undefined) | ||
t.describe('idb.add') | ||
await idb.add(store, 99, 42).promise() | ||
const idbVAdd = await idb.get(store, 42).promise() | ||
t.assert(idbVAdd === 99) | ||
t.describe('idb.addAutoKey') | ||
const key = await idb.addAutoKey(store, 1234).promise() | ||
const retrieved = await idb.get(store, key).promise() | ||
t.assert(retrieved === 1234) | ||
} | ||
|
||
/* c8 ignore next */ | ||
export const testBlocked = async () => { | ||
t.skip(!isBrowser) | ||
t.describe('ignore blocked event') | ||
await idb.deleteDB(testDBName).map(() => { | ||
const db = idb.openDB(testDBName, initTestDB) | ||
const transaction = createTransaction(db) | ||
const store = getStore(transaction) | ||
return pledge.all({ | ||
_req1: idb.put(store, 0, ['t', 1]), | ||
_req2: idb.put(store, 1, ['t', 2]), | ||
db | ||
}) | ||
}).map(({ db }) => { | ||
db.close() | ||
return idb.deleteDB(testDBName) | ||
}).promise() | ||
} | ||
|
||
export const testPerf = async () => { | ||
t.measureTime('resolve 1000 wait pledges') | ||
} |
Oops, something went wrong.