-
-
Save akovalev/d4de6587f5081592708ad0322df9ad1a to your computer and use it in GitHub Desktop.
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
import { EventEmitter } from "./event" | |
const print = console.log.bind(console) | |
const _indexedDB = (window.indexedDB | |
|| window["mozIndexedDB"] | |
|| window["webkitIndexedDB"] | |
|| window["msIndexedDB"]) as IDBFactory | |
// UpgradeFun is a function provided to Database.open and is called when an upgrade is needed. | |
// The function can modify object stores and indexes during immediate execution. | |
// In other words: Any code in a promise (or after await) can not modify object stores or indexes. | |
type UpgradeFun = (t :UpgradeTransaction)=>Promise<void> | |
interface StoreInfo { | |
readonly autoIncrement: boolean | |
readonly indexNames: DOMStringList | |
readonly keyPath: string | string[] | |
} | |
interface DatabaseSnapshot { | |
readonly storeNames :string[] | |
readonly storeInfo :Map<string,StoreInfo> | |
} | |
type RemoteChangeEvent = RemoteStoreChangeEvent | RemoteRecordChangeEvent | |
interface RemoteStoreChangeEvent { | |
store: string | |
type : "clear" | |
} | |
interface RemoteRecordChangeEvent { | |
store: string | |
type : "update" // record was updated (put or add) | |
| "delete" // record was removed | |
key: IDBValidKey | |
} | |
interface DatabaseEventMap { | |
"remotechange": RemoteChangeEvent | |
} | |
// interface Migrator { | |
// // upgradeSchema is called for every version step, i.e. upgrading from | |
// // version 3 -> 5 will call: | |
// // - upgradeSchema(3, 4, t) | |
// // - upgradeSchema(4, 5, t) | |
// upgradeSchema(prevVersion :number, newVersion :number, t :UpgradeTransaction) | |
// // migrateData is called only once and after all calls to upgradeSchema. | |
// migrateData(prevVersion :number, newVersion :number) | |
// } | |
// open opens a database, creating and/or upgrading it as needed. | |
export function open(name :string, version :number, upgradefun? :UpgradeFun) :Promise<Database> { | |
let db = new Database(name, version) | |
return db.open(upgradefun).then(() => db) | |
} | |
// delete removes an entire database | |
// | |
export { _delete as delete };function _delete(name :string, onblocked? :()=>void) :Promise<void> { | |
return new Promise<void>((resolve, reject) => { | |
let r = _indexedDB.deleteDatabase(name) | |
r.onblocked = onblocked || (() => { | |
r.onblocked = undefined | |
r.onsuccess = undefined | |
r.onerror = undefined | |
reject(new Error("db blocked")) | |
}) | |
r.onsuccess = () => { | |
if (navigator.userAgent.match(/Safari\//)) { | |
// Safari <=12.1.1 has a race condition bug where even after onsuccess is called, | |
// the database sometimes remains but without and actual object stores. | |
// This condition causes a subsequent open request to succeed without invoking an | |
// upgrade handler, and thus yielding an empty database. | |
// Running a second pass of deleteDatabase seem to work around this bug. | |
let r = _indexedDB.deleteDatabase(name) | |
r.onsuccess = () => { resolve() } | |
r.onerror = () => { reject(r.error) } | |
} else { | |
resolve() | |
} | |
} | |
r.onerror = () => { reject(r.error) } | |
}) | |
} | |
export class Database extends EventEmitter<DatabaseEventMap> { | |
readonly db :IDBDatabase // underlying object | |
readonly name :string | |
readonly version :number | |
// autoReopenOnClose controls if the database should be reopened and recreated when the | |
// user deletes the underlying data in their web browser. When this is false, all operations | |
// will suddenly start failing if the user deleted the database, while when this is true, | |
// operations always operate in transactions either on the old (before deleting) database or | |
// a new recreated database. | |
autoReopenOnClose :boolean = true | |
_isClosing = false | |
_lastSnapshot :DatabaseSnapshot | |
_broadcastChannel :BroadcastChannel|undefined | |
constructor(name :string, version :number) { | |
super() | |
this.name = name | |
this.version = version | |
} | |
// open the database, creating and/or upgrading it as needed using optional upgradefun | |
open(upgradefun? :UpgradeFun) :Promise<void> { | |
return openDatabase(this, upgradefun).then(() => { | |
this._setupCoordination() | |
return this._onopen(upgradefun) | |
}) | |
} | |
_reopen(upgradefun? :UpgradeFun) :Promise<void> { | |
// print("_reopen (database is closing)") | |
if (!this.autoReopenOnClose) { | |
return | |
} | |
this._isClosing = true | |
let db = this | |
let delayedTransactions :DelayedTransaction[] = [] | |
this.transaction = function(mode: IDBTransactionMode, ...stores :string[]) :Transaction { | |
let resolve :()=>void | |
let reject :(e:Error)=>void | |
let t = new DelayedTransaction((_resolve, _reject) => { | |
resolve = _resolve | |
reject = _reject | |
}) | |
t._resolve = resolve | |
t._reject = reject | |
t._db = db | |
t._stores = stores | |
t._mode = mode | |
delayedTransactions.push(t) | |
return t | |
} | |
// reopen | |
return openDatabase(this, upgradefun).then(db => { | |
this._isClosing = false | |
delete this.transaction // remove override | |
for (let t of delayedTransactions) { | |
t._flushDelayed() | |
} | |
return this._onopen(upgradefun) | |
}) | |
} | |
async _onopen(upgradefun? :UpgradeFun) :Promise<void> { | |
this._snapshot() | |
this.db.onclose = () => { this._reopen(upgradefun) } | |
} | |
_snapshot() { | |
let storeNames = this.storeNames | |
let storeInfo = new Map<string,StoreInfo>() | |
if (storeNames.length > 0) { | |
let t = this.db.transaction(storeNames, "readonly") | |
// @ts-ignore workaround for Safari bug | |
// eval("1+1") | |
for (let storeName of storeNames) { | |
let s = t.objectStore(storeName) | |
storeInfo.set(storeName, { | |
autoIncrement: s.autoIncrement, | |
indexNames: s.indexNames, | |
keyPath: s.keyPath, | |
} as StoreInfo) | |
} | |
} | |
this._lastSnapshot = { | |
storeNames, | |
storeInfo, | |
} | |
} | |
_setupCoordination() { | |
if (typeof BroadcastChannel == "undefined") { | |
// environment doesn't support BroadcastChannel. No coordination. | |
return | |
} | |
this._broadcastChannel = new BroadcastChannel(`xdb:${this.name}.${this.version}`) | |
this._broadcastChannel.onmessage = ev => { | |
this.emitEvent("remotechange", ev.data as RemoteChangeEvent) | |
} | |
} | |
_broadcastChange(ev :RemoteChangeEvent) { | |
if (this._broadcastChannel) { | |
this._broadcastChannel.postMessage(ev) | |
} | |
} | |
// close the database. | |
// | |
// The connection is not actually closed until all transactions created using this | |
// connection are complete. No new transactions can be created for this connection once | |
// this method is called. Methods that create transactions throw an exception if a closing | |
// operation is pending. | |
close() :void { | |
// Note: This does NOT cause a "close" event to be emitted. There's a disctinction | |
// between calling close() on a database object vs the "close" event occurring. | |
// | |
// - Calling close() causes the database object to become invalid and allows for | |
// Database.delete. | |
// | |
// - The "close" event occurs when the user deletes the database. However, any instance | |
// of a database object remains valid. | |
// | |
// A "close" event occurs when the database is deleted by the user, for instance | |
// via "delete website data" in web browser settings or use of developer console. | |
// Since that action renders the database object useless, we automatically reopen | |
// the database when this happens, and run the upgrade function which will handle | |
// initialization of the database. | |
// | |
this.db.close() | |
} | |
// storeNames is a list of objects store names that currently exist in the database. | |
// Use ObjectStore.indexNames to list indexes of a given store. | |
get storeNames() :string[] { return Array.from(this.db.objectStoreNames) } | |
// getStore starts a new transaction in mode on the named store. | |
// You can access the transaction via ObjectStore.transaction. | |
getStore(store :string, mode :IDBTransactionMode) :ObjectStore { | |
return this.transaction(mode, store).objectStore(store) | |
} | |
// transaction starts a new transaction. | |
// | |
// Its operations need to be defined immediately after creation. | |
// Transactions are Promises and complete when the transaction finishes. | |
// | |
// It's usually better to use read() or modify() instead of transaction() as those methods | |
// handle transaction promise as part of operation promises. | |
// | |
transaction(mode: IDBTransactionMode, ...stores :string[]) :Transaction { | |
return createTransaction(this, this.db.transaction(stores, mode)) | |
} | |
// modify encapsulates operations on object stores in one transaction. | |
// Returns the results of the input functions' results. | |
// | |
// This is uesful since working simply with transaction objects, it's easy to forget to catch | |
// all cases of promise resolution. Consider the following code: | |
// | |
// let [foo] = this.readwrite("foo") | |
// await foo.add({ message: "hello" }) | |
// await foo.transaction | |
// | |
// What happens if the record already exists and the "add" call fails? | |
// An error is thrown early and we never get to await the transaction (= unhandled rejection.) | |
// | |
// To fix this, we would need to rewrite the above code as: | |
// | |
// let [foo] = this.readwrite("foo") | |
// await Promise.all([ | |
// foo.add({ message: "hello" }), | |
// foo.transaction, | |
// ]) | |
// | |
// And that's exactly what this function does. We can rewrite the above as: | |
// | |
// await this.modify(["foo"], foo => foo.add({ message: "hello" })) | |
// | |
// For multiple independent operations, you can provide multiple functions: | |
// | |
// await this.modify(["foo"], | |
// foo => foo.add({ message: "hello" }), | |
// asnyc foo => { | |
// let msg = await foo.get("1") | |
// await foo.put({ message: msg.message + " (copy)" }) | |
// }, | |
// ) | |
// | |
modify(stores :string[], ...f :((...s:ObjectStore[])=>Promise<any>)[]) :Promise<any[]> { | |
let t = this.transaction("readwrite", ...stores) | |
let sv = stores.map(name => t.objectStore(name)) | |
return Promise.all([ t, ...f.map(f => f(...sv)) ]).then(r => (r.shift(), r)) | |
} | |
// read is like modify but operates on a read-only snapshot of the database. | |
// To read a single object, perfer to use get() instead. | |
// Returns the values of the input functions' results. | |
// | |
// Example of retrieving two objects: | |
// | |
// let [message, user] = await db.read(["messages", "users"], | |
// (m, _) => m.get("hello"), | |
// (_, u) => u.get("[email protected]") | |
// ) | |
// | |
read(stores :string[], ...f :((...s:ObjectStore[])=>Promise<any>)[]) :Promise<any[]> { | |
let t = this.transaction("readonly", ...stores) | |
let sv = stores.map(name => t.objectStore(name)) | |
return Promise.all(f.map(f => f(...sv))) // note: ignore transaction promise | |
} | |
// get a single object from store. See ObjectStore.get | |
get(store :string, query :IDBValidKey|IDBKeyRange) :Promise<any|null> { | |
return this.getStore(store, "readonly").get(query) | |
} | |
// getAll retrieves multiple values from store. See ObjectStore.getAll | |
getAll(store :string, query?: IDBValidKey|IDBKeyRange, count?: number): Promise<any[]> { | |
return this.getStore(store, "readonly").getAll(query, count) | |
} | |
// add a single object to store. See ObjectStore.add | |
add(store :string, value :any, key? :IDBValidKey): Promise<IDBValidKey> { | |
let s = this.getStore(store, "readwrite") | |
return Promise.all([s.add(value, key), s.transaction]).then(v => v[0]) | |
} | |
// put a single object in store. See ObjectStore.put | |
put(store :string, value: any, key?: IDBValidKey): Promise<IDBValidKey> { | |
let s = this.getStore(store, "readwrite") | |
return Promise.all([s.put(value, key), s.transaction]).then(v => v[0]) | |
} | |
// delete a single object from store. See ObjectStore.delete | |
delete(store :string, key :IDBValidKey|IDBKeyRange): Promise<void> { | |
let s = this.getStore(store, "readwrite") | |
return Promise.all([s.delete(key), s.transaction]) as any as Promise<void> | |
} | |
// getAllKeys retrieves all keys. See ObjectStore.getAllKeys | |
getAllKeys( | |
store :string, | |
query? :IDBValidKey|IDBKeyRange, | |
count? :number, | |
) :Promise<IDBValidKey[]> { | |
return this.getStore(store, "readonly").getAllKeys(query, count) | |
} | |
} | |
export class UpgradeTransaction { | |
readonly prevVersion :number | |
readonly nextVersion :number | |
_t :Transaction // the one transaction all upgrade operations share | |
db :Database | |
constructor(db :Database, t :IDBTransaction, prevVersion :number, nextVersion :number) { | |
this._t = createTransaction(db, t) | |
this.db = db | |
this.prevVersion = prevVersion | |
this.nextVersion = nextVersion | |
} | |
// storeNames is a list of objects store names that currently exist in the database. | |
// Use ObjectStore.indexNames to list indexes of a given store. | |
get storeNames() :string[] { return Array.from(this.db.db.objectStoreNames) } | |
// hasStore returns true if the database contains the named object store | |
hasStore(name :string) :boolean { | |
return this.db.db.objectStoreNames.contains(name) | |
} | |
// getStore retrieves the names object store | |
getStore(name :string) :ObjectStore { | |
return this._t.objectStore(name) | |
} | |
// createStore creates a new object store | |
createStore(name :string, params? :IDBObjectStoreParameters) :ObjectStore { | |
let os = this.db.db.createObjectStore(name, params) | |
return new ObjectStore(this.db, os, this._t) | |
} | |
// deleteStore deletes the object store with the given name | |
deleteStore(name :string) :void { | |
this.db.db.deleteObjectStore(name) | |
} | |
} | |
export class Transaction extends Promise<void> { | |
readonly db :Database | |
transaction :IDBTransaction // underlying transaction object | |
readonly aborted :boolean = false // true if abort() was called | |
// when true, abort() causes the transaction to be rejected (i.e. error.) | |
// when false, abort() causes the transaction to be fulfilled. | |
errorOnAbort :boolean = true | |
abort() :void { | |
;(this as any).aborted = true | |
this.transaction.abort() | |
} | |
objectStore(name :string) :ObjectStore { | |
return new ObjectStore(this.db, this.transaction.objectStore(name), this) | |
} | |
} | |
export class ObjectStore { | |
readonly db :Database | |
readonly store :IDBObjectStore | |
readonly transaction :Transaction // associated transaction | |
// autoIncrement is true if the store has a key generator | |
get autoIncrement() :boolean { return this.store.autoIncrement } | |
// indexNames is the names of indexes | |
get indexNames() :DOMStringList { return this.store.indexNames } | |
// name of the store. Note: Can be "set" _only_ within an upgrade transaction. | |
get name() :string { return this.store.name } | |
set name(name :string) { this.store.name = name } | |
constructor(db :Database, store :IDBObjectStore, transaction: Transaction) { | |
this.db = db | |
this.store = store | |
this.transaction = transaction | |
} | |
// clear deletes _all_ records in store | |
clear() :Promise<void> { | |
return this._promise(() => this.store.clear()).then(() => { | |
this.db._broadcastChange({ type:"clear", store: this.store.name }) | |
}) | |
} | |
// count the number of records matching the given key or key range in query | |
count(key? :IDBValidKey|IDBKeyRange) :Promise<number> { | |
return this._promise(() => this.store.count(key)) | |
} | |
// get retrieves the value of the first record matching the given key or key range in query. | |
// Returns undefined if there was no matching record. | |
get(query :IDBValidKey|IDBKeyRange) :Promise<any|undefined> { | |
return this._promise(() => this.store.get(query)) | |
} | |
// getAll retrieves the values of the records matching the given key or key range in query, | |
// up to count, if provided. | |
getAll(query? :IDBValidKey|IDBKeyRange, count? :number) :Promise<any[]> { | |
return this._promise(() => this.store.getAll(query, count)) | |
} | |
// Retrieves the key of the first record matching the given key or key range in query. | |
// Returns undefined if there was no matching key. | |
getKey(query :IDBValidKey|IDBKeyRange) :Promise<IDBValidKey|undefined> { | |
return this._promise(() => this.store.getKey(query)) | |
} | |
// getAllKeys retrieves the keys of records matching the given key or key range in query, | |
// up to count if given. | |
getAllKeys(query? :IDBValidKey|IDBKeyRange, count? :number) :Promise<IDBValidKey[]> { | |
return this._promise(() => this.store.getAllKeys(query, count)) | |
} | |
// add inserts a new record. If a record already exists in the object store with the key, | |
// then an error is raised. | |
add(value :any, key? :IDBValidKey) :Promise<IDBValidKey> { | |
return this._promise(() => this.store.add(value, key)).then(key => | |
(this.db._broadcastChange({ type: "update", store: this.store.name, key }), key) | |
) | |
} | |
// put inserts or updates a record. | |
put(value :any, key? :IDBValidKey) :Promise<IDBValidKey> { | |
return this._promise(() => this.store.put(value, key)).then(key => | |
(this.db._broadcastChange({ type: "update", store: this.store.name, key }), key) | |
) | |
} | |
// delete removes records in store with the given key or in the given key range in query. | |
// Deleting a record that does not exists does _not_ cause an error but succeeds. | |
delete(key :IDBValidKey|IDBKeyRange) :Promise<void> { | |
return this._promise(() => this.store.delete(key)).then(key => | |
(this.db._broadcastChange({ type: "delete", store: this.store.name, key }), key) | |
) | |
} | |
// createIndex creates a new index in store with the given name, keyPath and options and | |
// returns a new IDBIndex. If the keyPath and options define constraints that cannot be | |
// satisfied with the data already in store the upgrade transaction will abort with | |
// a "ConstraintError" DOMException. | |
// Throws an "InvalidStateError" DOMException if not called within an upgrade transaction. | |
createIndex(name :string, keyPath :string|string[], options? :IDBIndexParameters) :IDBIndex { | |
return this.store.createIndex(name, keyPath, options) | |
} | |
// deleteIndex deletes the index in store with the given name. | |
// Throws an "InvalidStateError" DOMException if not called within an upgrade transaction. | |
deleteIndex(name :string) :void { | |
this.store.deleteIndex(name) | |
} | |
// getIndex retrieves the named index. | |
getIndex(name :string) :Index { | |
return new Index(this, this.store.index(name)) | |
} | |
_promise<R,T extends IDBRequest = IDBRequest>(f :()=>IDBRequest<R>) :Promise<R> { | |
return new Promise<R>((resolve, reject) => { | |
let r = f() | |
r.onsuccess = () => { resolve(r.result) } | |
r.onerror = () => { reject(r.error) } | |
}) | |
} | |
} | |
class Index { | |
readonly store :ObjectStore | |
readonly index :IDBIndex | |
constructor(store :ObjectStore, index :IDBIndex) { | |
this.store = store | |
this.index = index | |
} | |
get keyPath(): string | string[] { return this.index.keyPath } | |
get multiEntry(): boolean { return this.index.multiEntry } | |
get name(): string { return this.index.name } | |
set name(v :string) { this.index.name = v } // only valid during upgrade | |
get unique(): boolean { return this.index.unique } | |
count(key? :IDBValidKey|IDBKeyRange) :Promise<number> { | |
return this._promise(() => this.index.count(key)) | |
} | |
get(key :IDBValidKey|IDBKeyRange) :Promise<any|undefined> { | |
return this._promise(() => this.index.get(key)) | |
} | |
getAll(query? :IDBValidKey|IDBKeyRange, count? :number) :Promise<any[]> { | |
return this._promise(() => this.index.getAll(query, count)) | |
} | |
// TOOD: implement more methods | |
_promise<R,T extends IDBRequest = IDBRequest>(f :()=>IDBRequest<R>) :Promise<R> { | |
return new Promise<R>((resolve, reject) => { | |
let r = f() | |
r.onsuccess = () => { resolve(r.result) } | |
r.onerror = () => { reject(r.error) } | |
}) | |
} | |
} | |
// used for zero-downtime database re-opening | |
class DelayedTransaction extends Transaction { | |
_db :Database | |
_mode :IDBTransactionMode | |
_stores :string[] | |
_resolve :()=>void | |
_reject :(e:Error)=>void | |
_delayed :DelayedObjectStore[] = [] | |
_flushDelayed() { | |
// print("DelayedTransaction _flushDelayed") | |
this.abort = Transaction.prototype.abort | |
this.objectStore = Transaction.prototype.objectStore | |
this.transaction = this._db.db.transaction(this._stores, this._mode) | |
activateDelayedTransaction(this) | |
if (this.aborted) { | |
this.abort() | |
} | |
for (let os of this._delayed) { | |
os._flushDelayed() | |
} | |
this._delayed = [] | |
} | |
abort() :void { | |
;(this as any).aborted = true | |
} | |
objectStore(name :string) :ObjectStore { | |
let info = this._db._lastSnapshot.storeInfo.get(name)! | |
if (!info) { throw new Error("object store not found") } | |
let os = new DelayedObjectStore( | |
this._db, | |
{ | |
name, | |
autoIncrement: info.autoIncrement, | |
indexNames: info.indexNames, | |
keyPath: info.keyPath, | |
} as any as IDBObjectStore, | |
this | |
) | |
// TODO: support ObjectStore.getIndex(name:string):IDBIndex | |
this._delayed.push(os) | |
return os | |
} | |
} | |
interface DelayedObjectStoreAction<R> { | |
resolve :(v:R)=>any | |
reject :(e:Error)=>void | |
f :()=>IDBRequest<R> | |
} | |
class DelayedObjectStore extends ObjectStore { | |
_delayed :DelayedObjectStoreAction<any>[] = [] | |
_flushDelayed() { | |
// print("DelayedObjectStore _flushDelayed", this._delayed) | |
;(this as any).store = this.transaction.transaction.objectStore(this.store.name) | |
this._promise = ObjectStore.prototype._promise | |
this._delayed.forEach(({ f, resolve, reject }) => { | |
let r = f() | |
r.onsuccess = () => { resolve(r.result) } | |
r.onerror = () => { reject(r.error) } | |
}) | |
this._delayed = [] | |
} | |
_promise<R,T extends IDBRequest = IDBRequest>(f :()=>IDBRequest<R>) :Promise<R> { | |
return new Promise<R>((resolve, reject) => { | |
this._delayed.push({ f, resolve, reject }) | |
}) | |
} | |
} | |
// export class MemoryDatabase extends Database { | |
// close() :void {} | |
// open(upgradefun? :UpgradeFun) :Promise<void> { | |
// return Promise.resolve() | |
// } | |
// transaction(mode: IDBTransactionMode, ...stores :string[]) :MemTransaction { | |
// return new MemTransaction(this, stores, mode) | |
// } | |
// } | |
// class MemObjectStore extends ObjectStore { | |
// _data = new Map<any,any>() | |
// clear() :Promise<void> { | |
// this._data.clear() | |
// return Promise.resolve() | |
// } | |
// count(key? :IDBValidKey|IDBKeyRange) :Promise<number> { | |
// // TODO count keys | |
// return Promise.resolve(this._data.size) | |
// } | |
// get(query :IDBValidKey|IDBKeyRange) :Promise<any|undefined> { | |
// return Promise.resolve(this._data.get(query)) | |
// } | |
// getAll(query? :IDBValidKey|IDBKeyRange, count? :number) :Promise<any[]> { | |
// // TOOD: get multiple | |
// return Promise.resolve(this._data.get(query)) | |
// } | |
// getKey(query :IDBValidKey|IDBKeyRange) :Promise<IDBValidKey|undefined> { | |
// let key :IDBValidKey|undefined | |
// for (let k of this._data.keys()) { | |
// key = k as IDBValidKey | |
// break | |
// } | |
// return Promise.resolve(key) | |
// } | |
// getAllKeys(query? :IDBValidKey|IDBKeyRange, count? :number) :Promise<IDBValidKey[]> { | |
// return Promise.resolve(Array.from(this._data.keys())) | |
// } | |
// add(value :any, key? :IDBValidKey) :Promise<IDBValidKey> { | |
// if (this._data.has(key)) { | |
// } | |
// } | |
// // put inserts or updates a record. | |
// put(value :any, key? :IDBValidKey) :Promise<IDBValidKey> { | |
// return this._promise(() => this.store.put(value, key)).then(key => | |
// (this.db._broadcastChange({ type: "update", store: this.store.name, key }), key) | |
// ) | |
// } | |
// // delete removes records in store with the given key or in the given key range in query. | |
// // Deleting a record that does not exists does _not_ cause an error but succeeds. | |
// delete(key :IDBValidKey|IDBKeyRange) :Promise<void> { | |
// return this._promise(() => this.store.delete(key)).then(key => | |
// (this.db._broadcastChange({ type: "delete", store: this.store.name, key }), key) | |
// ) | |
// } | |
// } | |
// class MemTransaction extends Transaction { | |
// db :MemoryDatabase | |
// stores :string[] | |
// mode :IDBTransactionMode | |
// constructor(db :MemoryDatabase, stores :string[], mode: IDBTransactionMode) { | |
// super(resolve => resolve()) | |
// this.db = db | |
// this.stores = stores | |
// this.mode = mode | |
// } | |
// abort() :void { | |
// ;(this as any).aborted = true | |
// } | |
// objectStore(name :string) :ObjectStore { | |
// let os = new MemObjectStore( | |
// this._db, | |
// { | |
// name, | |
// autoIncrement: info.autoIncrement, | |
// indexNames: info.indexNames, | |
// keyPath: info.keyPath, | |
// } as any as IDBObjectStore, | |
// this | |
// ) | |
// // TODO: support ObjectStore.getIndex(name:string):IDBIndex | |
// this._delayed.push(os) | |
// return os | |
// } | |
// } | |
function activateDelayedTransaction(t :DelayedTransaction) { | |
let tr = t.transaction | |
let resolve = t._resolve ; (t as any)._resolve = undefined | |
let reject = t._reject ; (t as any)._reject = undefined | |
tr.oncomplete = () => { resolve() } | |
tr.onerror = () => { reject(tr.error) } | |
tr.onabort = () => { | |
;(t as any).aborted = true | |
if (t.errorOnAbort) { | |
reject(new Error("transaction aborted")) | |
} else { | |
resolve() | |
} | |
} | |
} | |
function createTransaction(db :Database, tr :IDBTransaction) :Transaction { | |
// Note: For complicated reasons related to Promise implementation in V8, we | |
// can't customize the Transaction constructor. | |
var t = new Transaction((resolve, reject) => { | |
tr.oncomplete = () => { resolve() } | |
tr.onerror = () => { reject(tr.error) } | |
tr.onabort = () => { | |
;(t as any).aborted = true | |
if (t.errorOnAbort) { | |
reject(new Error("transaction aborted")) | |
} else { | |
resolve() | |
} | |
} | |
}) | |
;(t as any).db = db | |
t.transaction = tr | |
return t | |
} | |
function openDatabase(db :Database, upgradef? :UpgradeFun) :Promise<IDBDatabase> { | |
return new Promise<IDBDatabase>((_resolve, reject) => { | |
// Note on pcount: upgradef may take an arbitrary amount of time to complete. | |
// The semantics of open() are so that open() should complete when the database | |
// is ready for use, meaning we need to include any upgrades into the "open" action. | |
// The onsuccess handler is called whenever the last operation in an update function | |
// completes, which may be sooner than when other code in the upgrade function completes, | |
// like for instance "post upgrade" code run that e.g. logs on the network. | |
// To solve for this, we simply count promises with pcount and resolve when pcount | |
// reaches zero (no outstanding processes.) | |
let openReq = _indexedDB.open(db.name, db.version) | |
let pcount = 1 // promise counter; starts with 1 outstanding process (the "open" action) | |
let resolve = () => { | |
if (--pcount == 0) { | |
;(db as any).db = openReq.result | |
_resolve() | |
} | |
} | |
if (upgradef) { | |
openReq.onupgradeneeded = ev => { | |
;(db as any).db = openReq.result | |
let u = new UpgradeTransaction(db, openReq.transaction, ev.oldVersion, ev.newVersion) | |
let onerr = err => { | |
// abort the upgrade if upgradef fails | |
try { u._t.abort() } catch(_) {} | |
reject(err) | |
} | |
pcount++ | |
u._t.then(resolve).catch(onerr) | |
pcount++ | |
try { | |
upgradef(u).then(resolve).catch(onerr) | |
} catch (err) { | |
onerr(err) | |
} | |
} | |
} | |
openReq.onsuccess = () => { resolve() } | |
openReq.onerror = () => { reject(openReq.error) } | |
}) | |
} | |
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
import * as xdb from "./xdb" | |
const print = console.log.bind(console) | |
const DB_VERSION = 1 | |
let db :xdb.Database | |
async function openDatabase() { | |
print("deleting database") | |
await xdb.delete("mydb", () => print("delete is blocked -- waiting")) | |
print("opening database") | |
db = await xdb.open("mydb", DB_VERSION, async t => { | |
print(`upgrade database ${t.prevVersion} -> ${t.nextVersion}`) | |
let articles = t.createStore("articles", { keyPath: "id" }) | |
// articles.createIndex("hours", "hours", { unique: false }) | |
await articles.add({ id: "hello", code: `print("hello")` }) | |
// let rec = await articles.get("hello") | |
// let store2 = t.createStore("articles2", { keyPath: "id" }) | |
print("upgrade done") | |
}) | |
print("opened database") | |
let script = await db.get("articles", "hello") | |
print(`db.get("articles", "hello") =>`, script) | |
script = await db.get("articles", "helloz") | |
print(`db.get("articles", "helloz") =>`, script) | |
await db.put("articles", { id: "meow", meow: "Meow meow" }) | |
print(`db.get("articles", "meow") =>`, await db.get("articles", "meow")) | |
print("getAll =>", await db.getAll("articles")) | |
print("getAllKeys =>", await db.getAllKeys("articles")) | |
// print("deleting a record that exists") | |
// await db.modify(["articles"], s => s.delete("hello")) | |
// print("deleting a record that does not exist") | |
// await db.modify(["articles"], s => s.delete("hellozzzz")) | |
;[script] = await db.read(["articles"], s => s.get("hello")) | |
print(`db.read(["articles"], s => s.get("hello")) =>`, script) | |
// await new Promise(r => setTimeout(r, 100)) | |
// simulate the user deleting the database in the browser | |
// db.db.onclose(undefined as any as Event) | |
// print("db.get()...") | |
// script = await db.get("articles", "hello") | |
// print("script:", script) | |
print("db.put() ...") | |
let key = await db.put("articles", { id: "closetest", message: "close event test" }) | |
print(`db.put("articles", {...}) => ${key}`) | |
script = await db.get("articles", "closetest") | |
print("script:", script) | |
// setInterval( | |
// () => { | |
// (async () => { | |
// await db.put("articles", { id: "closetest", message: "close event test" }) | |
// script = await db.get("articles", "closetest") | |
// print("script:", script) | |
// })().catch(err => console.error(err.stack)) | |
// }, | |
// 1000 | |
// ) | |
// print("making transaction and aborting it") | |
// try { | |
// let t = db.transaction("readonly", "articles") | |
// t.abort() | |
// await t | |
// print("transaction completed") | |
// } catch (e) { | |
// print("transaction failed: " + e) | |
// } | |
} |
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
type EventHandler<T=any> = (data :T)=>void | |
const events = Symbol('events') | |
export class EventEmitter<EventMap = {[k:string]:any}> { | |
_events :Map<keyof EventMap,Set<EventHandler>> | |
addListener<K extends keyof EventMap>(e :K, handler :EventHandler<EventMap[K]>) { | |
let m = this._events | |
let s :Set<EventHandler> | |
if (!m) { | |
this._events = m = new Map() | |
} else if (s = m.get(e)) { | |
s.add(handler) | |
return | |
} | |
s = new Set<EventHandler>([handler]) | |
m.set(e, s) | |
} | |
on<K extends keyof EventMap>(e :K, handler :EventHandler<EventMap[K]>) { | |
return this.addListener(e, handler) | |
} | |
removeListener<K extends keyof EventMap>(e :K, handler :EventHandler<EventMap[K]>) { | |
let m = this._events | |
let s :Set<EventHandler> | |
if (m && (s = m.get(e))) { | |
s.delete(handler) | |
if (s.size == 0) { | |
m.delete(e) | |
} | |
} | |
} | |
emitEvent<K extends keyof EventMap>(e :K, data? :EventMap[K]) { | |
let m = this._events | |
let s :Set<EventHandler<EventMap[K]>> | |
if (m && (s = m.get(e))) { | |
for (let handler of s) { | |
handler(data) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment