diff options
author | Florian Dold <florian.dold@gmail.com> | 2017-06-08 12:15:45 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2017-06-08 12:15:45 +0200 |
commit | f93839558d528f600b10fbe816914e7ff91fca64 (patch) | |
tree | c7ff2154abef357a532e6ffc90e299ea598db670 /src/memidb/memidb.ts | |
parent | 673ecb01efa32e4dcecc9f8595a8e13699b842f4 (diff) | |
download | wallet-core-dold/dev/memidb.tar.gz wallet-core-dold/dev/memidb.tar.bz2 wallet-core-dold/dev/memidb.zip |
aa treesdold/dev/memidb
Diffstat (limited to 'src/memidb/memidb.ts')
-rw-r--r-- | src/memidb/memidb.ts | 961 |
1 files changed, 961 insertions, 0 deletions
diff --git a/src/memidb/memidb.ts b/src/memidb/memidb.ts new file mode 100644 index 000000000..4ad3a01b9 --- /dev/null +++ b/src/memidb/memidb.ts @@ -0,0 +1,961 @@ +/* + This file is part of TALER + (C) 2017 Inria and GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * In-memory implementation of the IndexedDB interface. + * + * Transactions support rollback, but they are all run sequentially within the + * same MemoryIDBFactory. + * + * Every operation involves copying the whole database state, making it only + * feasible for small databases. + */ + +/* work in progres ... */ +/* tslint:disable */ + + +const structuredClone = require("structured-clone"); + + +interface Store { + name: string; + keyPath?: string | string[]; + keyGenerator: number; + autoIncrement: boolean; + objects: { [primaryKey: string]: any }; + indices: { [indexName: string]: Index }; +} + +interface Index { + multiEntry: boolean; + unique: boolean; + + /** + * Map the index's key to the primary key. + */ + map: { [indexKey: string]: string[] }; +} + + +interface Database { + name: string; + version: number; + stores: { [name: string]: Store }; +} + + +interface Databases { + [name: string]: Database; +} + + +/** + * Resolved promise, used to schedule various things + * by calling .next on it. + */ +const alreadyResolved = Promise.resolve(); + + +class MyDomStringList extends Array<string> implements DOMStringList { + contains(s: string) { + for (let i = 0; i < this.length; i++) { + if (s === this[i]) { + return true; + } + } + return false; + } + item(i: number) { + return this[i]; + } +} + + + + +class MyKeyRange implements IDBKeyRange { + static only(value: any): IDBKeyRange { + return new MyKeyRange(value, value, false, false); + } + + static bound(lower: any, upper: any, lowerOpen: boolean = false, upperOpen: boolean = false) { + return new MyKeyRange(lower, upper, lowerOpen, upperOpen); + } + + static lowerBound(lower: any, lowerOpen: boolean = false) { + return new MyKeyRange(lower, undefined, lowerOpen, true); + } + + static upperBound(upper: any, upperOpen: boolean = false) { + return new MyKeyRange(undefined, upper, true, upperOpen); + } + + constructor(public lower: any, public upper: any, public lowerOpen: boolean, public upperOpen: boolean) { + } +} + + +/** + * Type guard for an IDBKeyRange. + */ +export function isKeyRange(obj: any): obj is IDBKeyRange { + return (typeof obj === "object" && + "lower" in obj && "upper" in obj && + "lowerOpen" in obj && "upperOpen" in obj); +} + + +export function compareKeys(a: any, b: any): -1|0|1 { + throw Error("not implemented") +} + + +class IndexHandle implements IDBIndex { + + _unique: boolean; + _multiEntry: boolean; + + get keyPath(): string | string[] { + throw Error("not implemented"); + } + + get name () { + return this.indexName; + } + + get unique() { + return this._unique; + } + + get multiEntry() { + return this._multiEntry; + } + + constructor(public objectStore: MyObjectStore, public indexName: string) { + } + + count(key?: IDBKeyRange | IDBValidKey): IDBRequest { + throw Error("not implemented"); + } + + get(key: IDBKeyRange | IDBValidKey): IDBRequest { + throw Error("not implemented"); + } + + getKey(key: IDBKeyRange | IDBValidKey): IDBRequest { + throw Error("not implemented"); + } + + openCursor(range?: IDBKeyRange | IDBValidKey, direction?: IDBCursorDirection): IDBRequest { + throw Error("not implemented"); + } + + openKeyCursor(range?: IDBKeyRange | IDBValidKey, direction?: IDBCursorDirection): IDBRequest { + throw Error("not implemented"); + } +} + +class MyRequest implements IDBRequest { + onerror: (this: IDBRequest, ev: Event) => any; + + onsuccess: (this: IDBRequest, ev: Event) => any; + successHandlers: Array<(this: IDBRequest, ev: Event) => any> = []; + + done: boolean = false; + _result: any; + + _source: (IDBObjectStore | IDBIndex | IDBCursor | null) = null; + + constructor(public _transaction: Transaction, public runner: () => void) { + } + + callSuccess() { + const ev = new MyEvent("success", this); + if (this.onsuccess) { + this.onsuccess(ev); + } + for (let h of this.successHandlers) { + h.call(this, ev); + } + } + + get error(): DOMException { + return (null as any) as DOMException; + } + + get result(): any { + return this._result; + } + + get source() { + // buggy type definitions don't allow null even though it's in + // the spec. + return this._source as (IDBObjectStore | IDBIndex | IDBCursor); + } + + get transaction() { + return this._transaction; + } + + dispatchEvent(evt: Event): boolean { + return false; + } + + get readyState() { + if (this.done) { + return "done"; + } + return "pending"; + } + + removeEventListener(type: string, + listener?: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions): void { + throw Error("not implemented"); + } + + addEventListener(type: string, + listener: EventListenerOrEventListenerObject, + useCapture?: boolean): void { + switch (type) { + case "success": + this.successHandlers.push(listener as any); + break; + } + } +} + +class OpenDBRequest extends MyRequest implements IDBOpenDBRequest { + onblocked: (this: IDBOpenDBRequest, ev: Event) => any; + + onupgradeneeded: (this: IDBOpenDBRequest, ev: IDBVersionChangeEvent) => any; + upgradeneededHandlers: Array<(this: IDBOpenDBRequest, ev: IDBVersionChangeEvent) => any> = []; + + callOnupgradeneeded(ev: IDBVersionChangeEvent) { + if (this.onupgradeneeded) { + this.onupgradeneeded(ev); + } + for (let h of this.upgradeneededHandlers) { + h.call(this, ev); + } + } + + removeEventListener(type: string, + listener?: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions): void { + throw Error("not implemented"); + } + + addEventListener(type: string, + listener: EventListenerOrEventListenerObject, + useCapture?: boolean): void { + switch (type) { + case "upgradeneeded": + this.upgradeneededHandlers.push(listener as any); + break; + default: + super.addEventListener(type, listener, useCapture); + } + } +} + +function follow(x: any, s: string, replacement?: any): any { + if (s === "") { + return x; + } + const ptIdx = s.indexOf("."); + if (ptIdx < 0) { + const v = x[s]; + if (replacement !== undefined) { + x[s] = replacement; + } + return v; + } else { + const identifier = s.substring(0, ptIdx); + const rest = s.substring(ptIdx + 1); + return follow(x[identifier], rest, replacement); + } +} + +export function evaluateKeyPath(x: any, path: string | string[], replacement?: any): any { + if (typeof path === "string") { + return follow(x, path, replacement); + } else if (Array.isArray(path)) { + const res: any[] = []; + for (let s of path) { + let c = follow(x, s, replacement); + if (c === undefined) { + return undefined; + } + res.push(c); + } + return res; + } else { + throw Error("invalid key path, must be string or array of strings"); + } +} + +function stringifyKey(key: any) { + return JSON.stringify(key); +} + +export function isValidKey(key: any, memo: any[] = []) { + if (typeof key === "string" || typeof key === "number" || key instanceof Date) { + return true; + } + if (Array.isArray(key)) { + for (const element of key) { + if (!isValidKey(element, memo.concat([key]))) { + return false; + } + } + return true; + } + return false; +} + +class MyObjectStore implements IDBObjectStore { + + _keyPath: string | string[] | undefined; + _autoIncrement: boolean; + + get indexNames() { + return new DOMStringList(); + } + + constructor(public transaction: Transaction, public storeName: string) { + this._keyPath = this.transaction.transactionDbData.stores[this.storeName].keyPath as (string | string[]); + this._autoIncrement = this.transaction.transactionDbData.stores[this.storeName].autoIncrement; + } + + get keyPath(): string | string[] { + // TypeScript definitions are wrong here and don't permit a null keyPath + return this._keyPath as (string | string[]); + } + + get name() { + return this.storeName; + } + + get autoIncrement() { + return this._autoIncrement; + } + + storeImpl(originalValue: any, key: any|undefined, allowExisting: boolean) { + if (this.transaction.mode === "readonly") { + throw Error(); + } + if (!this.transaction.active) { + throw Error(); + } + if (!this.transaction.transactionDbData.stores.hasOwnProperty(this.storeName)) { + throw Error("object store was deleted"); + } + + const store = this.transaction.transactionDbData.stores[this.storeName]; + + const value = structuredClone(originalValue); + + if (this.keyPath) { + // we're dealine with in-line keys + if (key) { + throw Error("keys not allowed with in-line keys"); + } + key = evaluateKeyPath(value, this.keyPath); + if (!key && !this.autoIncrement) { + throw Error("key path must evaluate to key for in-line stores without autoIncrement"); + } + if (this.autoIncrement) { + if (key && typeof key === "number") { + store.keyGenerator = key + 1; + } else { + key = store.keyGenerator; + store.keyGenerator += 1; + evaluateKeyPath(value, this.keyPath, key); + } + } + } else { + // we're dealing with out-of-line keys + if (!key && !this.autoIncrement) { + throw Error("key must be provided for out-of-line stores without autoIncrement"); + } + key = this.transaction.transactionDbData.stores + if (this.autoIncrement) { + if (key && typeof key === "number") { + store.keyGenerator = key + 1; + } else { + key = store.keyGenerator; + store.keyGenerator += 1; + } + } + } + + const stringKey = stringifyKey(key); + + if (store.objects.hasOwnProperty(stringKey) && !allowExisting) { + throw Error("key already exists"); + } + + const req = new MyRequest(this.transaction, () => { + req._source = this; + store.objects[stringKey] = value; + }); + return req; + } + + put(value: any, key?: any): IDBRequest { + return this.storeImpl(value, key, true); + } + + add(value: any, key?: any): IDBRequest { + return this.storeImpl(value, key, false); + } + + delete(key: any): IDBRequest { + if (this.transaction.mode === "readonly") { + throw Error(); + } + if (!this.transaction.active) { + throw Error(); + } + if (!this.transaction.transactionDbData.stores.hasOwnProperty(this.storeName)) { + throw Error("object store was deleted"); + } + const store = this.transaction.transactionDbData.stores[this.storeName]; + const stringKey = stringifyKey(key); + const req = new MyRequest(this.transaction, () => { + req._source = this; + delete store.objects[stringKey]; + }); + return req; + } + + get(key: any): IDBRequest { + if (!this.transaction.active) { + throw Error(); + } + if (!this.transaction.transactionDbData.stores.hasOwnProperty(this.storeName)) { + throw Error("object store was deleted"); + } + if (isKeyRange(key)) { + throw Error("not implemented"); + } + const store = this.transaction.transactionDbData.stores[this.storeName]; + const stringKey = stringifyKey(key); + const req = new MyRequest(this.transaction, () => { + req._source = this; + req._result = store.objects[stringKey]; + }); + return req; + } + + deleteIndex(indexName: string) { + throw Error("not implemented"); + } + + clear(): IDBRequest { + throw Error("not implemented"); + } + + count(key?: any): IDBRequest { + throw Error("not implemented"); + } + + createIndex(name: string, keyPath: string | string[], optionalParameters?: IDBIndexParameters): IDBIndex { + throw Error("not implemented"); + } + + index(indexName: string): IDBIndex { + return new IndexHandle(this, indexName); + } + + openCursor(range?: IDBKeyRange | IDBValidKey, direction?: IDBCursorDirection): IDBRequest { + throw Error("not implemented"); + } +} + + +class Db implements IDBDatabase { + + onabort: (this: IDBDatabase, ev: Event) => any; + onerror: (this: IDBDatabase, ev: Event) => any; + onversionchange: (ev: IDBVersionChangeEvent) => any; + + _storeNames: string[] = []; + + constructor(private _name: string, private _version: number, private factory: MemoryIDBFactory) { + for (let storeName in this.dbData.stores) { + if (this.dbData.stores.hasOwnProperty(storeName)) { + this._storeNames.push(storeName); + } + } + this._storeNames.sort(); + } + + get dbData(): Database { + return this.factory.data[this._name]; + } + + set dbData(data) { + this.factory.data[this._name] = data; + } + + get name() { + return this._name; + } + + get objectStoreNames() { + return new MyDomStringList(...this._storeNames); + } + + get version() { + return this._version; + } + + close() { + } + + createObjectStore(name: string, optionalParameters?: IDBObjectStoreParameters): IDBObjectStore { + let tx = this.factory.getTransaction(); + if (tx.mode !== "versionchange") { + throw Error("invalid mode"); + } + + const td = tx.transactionDbData; + if (td.stores[name]) { + throw Error("object store already exists"); + } + + td.stores[name] = { + autoIncrement: !!(optionalParameters && optionalParameters.autoIncrement), + indices: {}, + keyGenerator: 1, + name, + objects: [], + }; + + this._storeNames.push(name); + this._storeNames.sort(); + + return new MyObjectStore(tx, name); + } + + deleteObjectStore(name: string): void { + let tx = this.factory.getTransaction(); + if (tx.mode !== "versionchange") { + throw Error("invalid mode"); + } + + const td = tx.transactionDbData; + if (td.stores[name]) { + throw Error("object store does not exists"); + } + + const idx = this._storeNames.indexOf(name); + if (idx < 0) { + throw Error(); + } + this._storeNames.splice(idx, 1); + + delete td.stores[name]; + } + + transaction(storeNames: string | string[], mode: IDBTransactionMode = "readonly"): IDBTransaction { + const tx = new Transaction(this._name, this, mode); + return tx; + } + + dispatchEvent(evt: Event): boolean { + throw Error("not implemented"); + } + + removeEventListener(type: string, + listener?: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions): void { + throw Error("not implemented"); + } + + addEventListener(type: string, + listener: EventListenerOrEventListenerObject, + useCapture?: boolean): void { + throw Error("not implemented"); + } +} + +enum TransactionState { + Created = 1, + Running = 2, + Commited = 3, + Aborted = 4, +} + +class Transaction implements IDBTransaction { + readonly READ_ONLY: string = "readonly"; + readonly READ_WRITE: string = "readwrite"; + readonly VERSION_CHANGE: string = "versionchange"; + + onabort: (this: IDBTransaction, ev: Event) => any; + onerror: (this: IDBTransaction, ev: Event) => any; + oncomplete: (this: IDBTransaction, ev: Event) => any; + + completeHandlers: Array<(this: IDBTransaction, ev: Event) => any> = []; + + state: TransactionState = TransactionState.Created; + + _transactionDbData: Database|undefined; + + constructor(public dbName: string, public dbHandle: Db, public _mode: IDBTransactionMode) { + } + + get mode() { + return this._mode; + } + + get active(): boolean { + return this.state === TransactionState.Running || this.state === TransactionState.Created; + } + + start() { + if (this.state != TransactionState.Created) { + throw Error(); + } + this.state = TransactionState.Running; + this._transactionDbData = structuredClone(this.dbHandle.dbData); + if (!this._transactionDbData) { + throw Error(); + } + } + + commit() { + if (this.state != TransactionState.Running) { + throw Error(); + } + if (!this._transactionDbData) { + throw Error(); + } + this.state = TransactionState.Commited; + this.dbHandle.dbData = this._transactionDbData; + } + + get error(): DOMException { + throw Error("not implemented"); + } + + get db() { + return this.dbHandle; + } + + get transactionDbData() { + if (this.state != TransactionState.Running) { + throw Error(); + } + let d = this._transactionDbData; + if (!d) { + throw Error(); + } + return d; + } + + abort() { + throw Error("not implemented"); + } + + objectStore(storeName: string): IDBObjectStore { + return new MyObjectStore(this, storeName); + } + + dispatchEvent(evt: Event): boolean { + throw Error("not implemented"); + } + + removeEventListener(type: string, + listener?: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions): void { + throw Error("not implemented"); + } + + addEventListener(type: string, + listener: EventListenerOrEventListenerObject, + useCapture?: boolean): void { + switch (type) { + case "complete": + this.completeHandlers.push(listener as any); + break; + } + } + + callComplete(ev: Event) { + if (this.oncomplete) { + this.oncomplete(ev); + } + for (let h of this.completeHandlers) { + h.call(this, ev); + } + } +} + + +/** + * Polyfill for CustomEvent. + */ +class MyEvent implements Event { + readonly NONE: number = 0; + readonly CAPTURING_PHASE: number = 1; + readonly AT_TARGET: number = 2; + readonly BUBBLING_PHASE: number = 3; + + _bubbles = false; + _cancelable = false; + _target: any; + _currentTarget: any; + _defaultPrevented: boolean = false; + _eventPhase: number = 0; + _timeStamp: number = 0; + _type: string; + + constructor(typeArg: string, target: any) { + this._type = typeArg; + this._target = target; + } + + get eventPhase() { + return this._eventPhase; + } + + get returnValue() { + return this.defaultPrevented; + } + + set returnValue(v: boolean) { + if (v) { + this.preventDefault(); + } + } + + get isTrusted() { + return false; + } + + get bubbles() { + return this._bubbles; + } + + get cancelable() { + return this._cancelable; + } + + set cancelBubble(v: boolean) { + if (v) { + this.stopPropagation(); + } + } + + get defaultPrevented() { + return this._defaultPrevented; + } + + stopPropagation() { + throw Error("not implemented"); + } + + get currentTarget() { + return this._currentTarget; + } + + get target() { + return this._target; + } + + preventDefault() { + } + + get srcElement() { + return this.target; + } + + get timeStamp() { + return this._timeStamp; + } + + get type() { + return this._type; + } + + get scoped() { + return false; + } + + initEvent(eventTypeArg: string, canBubbleArg: boolean, cancelableArg: boolean) { + if (this._eventPhase != 0) { + return; + } + + this._type = eventTypeArg; + this._bubbles = canBubbleArg; + this._cancelable = cancelableArg; + } + + stopImmediatePropagation() { + throw Error("not implemented"); + } + + deepPath(): EventTarget[] { + return []; + } +} + + +class VersionChangeEvent extends MyEvent { + _newVersion: number|null; + _oldVersion: number; + constructor(oldVersion: number, newVersion: number|null, target: any) { + super("VersionChange", target); + this._oldVersion = oldVersion; + this._newVersion = newVersion; + } + + get newVersion() { + return this._newVersion; + } + + get oldVersion() { + return this._oldVersion; + } +} + + +export class MemoryIDBFactory implements IDBFactory { + data: Databases = {}; + + currentRequest: MyRequest|undefined; + + scheduledRequests: MyRequest[] = []; + + private addRequest(r: MyRequest) { + this.scheduledRequests.push(r); + if (this.currentRequest) { + return; + } + const runNext = (prevRequest?: MyRequest) => { + const nextRequest = this.scheduledRequests.shift(); + if (nextRequest) { + const tx = nextRequest.transaction; + + if (tx.state === TransactionState.Running) { + // Okay, we're continuing with the same transaction + } else if (tx.state === TransactionState.Created) { + tx.start(); + } else { + throw Error(); + } + + this.currentRequest = nextRequest; + this.currentRequest.runner(); + this.currentRequest.done = true; + this.currentRequest = undefined; + runNext(nextRequest); + } else if (prevRequest) { + // We have no other request scheduled, so + // auto-commit the transaction that the + // previous request worked on. + let lastTx = prevRequest._transaction; + lastTx.commit(); + } + }; + alreadyResolved.then(() => { + runNext(); + }); + } + + /** + * Get the only transaction that is active right now + * or throw if no transaction is active. + */ + getTransaction() { + const req = this.currentRequest; + if (!req) { + throw Error(); + } + return req.transaction; + } + + cmp(a: any, b: any): number { + throw Error("not implemented"); + } + + deleteDatabase(name: string): IDBOpenDBRequest { + throw Error("not implemented"); + } + + open(dbName: string, version?: number): IDBOpenDBRequest { + if (version !== undefined && version <= 0) { + throw Error("invalid version"); + } + + let upgradeNeeded = false; + let oldVersion: number; + let mydb: Database; + if (dbName in this.data) { + mydb = this.data[dbName]; + if (!mydb) { + throw Error(); + } + oldVersion = mydb.version; + if (version === undefined || version == mydb.version) { + // we can open without upgrading + } else if (version > mydb.version) { + upgradeNeeded = true; + mydb.version = version; + } else { + throw Error("version error"); + } + } else { + mydb = { + name: dbName, + stores: {}, + version: (version || 1), + }; + upgradeNeeded = true; + oldVersion = 0; + } + + this.data[dbName] = mydb; + + const db = new Db(dbName, mydb.version, this); + const tx = new Transaction(dbName, db, "versionchange"); + + const req = new OpenDBRequest(tx, () => { + req._result = db; + if (upgradeNeeded) { + let versionChangeEvt = new VersionChangeEvent(oldVersion, mydb.version, db); + req.callOnupgradeneeded(versionChangeEvt); + } + req.callSuccess(); + }); + + this.addRequest(req); + + return req; + } +} + +/** + * Inject our IndexedDb implementation in the global namespace, + * potentially replacing an existing implementation. + */ +export function injectGlobals() { +} |