Skip to content

Commit

Permalink
experimenting with new pledge replacement for promises
Browse files Browse the repository at this point in the history
  • Loading branch information
dmonad committed Mar 23, 2024
1 parent 3317c65 commit 7cbd84d
Show file tree
Hide file tree
Showing 8 changed files with 718 additions and 5 deletions.
2 changes: 1 addition & 1 deletion indexeddb.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,5 @@ export const testBlocked = async () => {
await idb.put(store, 0, ['t', 1])
await idb.put(store, 1, ['t', 2])
db.close()
idb.deleteDB(testDBName)
await idb.deleteDB(testDBName)
}
265 changes: 265 additions & 0 deletions indexeddbV2.js
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 */
118 changes: 118 additions & 0 deletions indexeddbV2.test.js
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')
}
Loading

0 comments on commit 7cbd84d

Please sign in to comment.