diff options
-rw-r--r-- | src/memidb.ts | 585 | ||||
-rw-r--r-- | tsconfig.json | 4 |
2 files changed, 586 insertions, 3 deletions
diff --git a/src/memidb.ts b/src/memidb.ts index fe1d986f1..36607d71f 100644 --- a/src/memidb.ts +++ b/src/memidb.ts @@ -16,12 +16,22 @@ /** * 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"); +const structuredSerialize = require("structured-clone").serialize; + + interface StoredObject { key: any; object: string; @@ -30,6 +40,8 @@ interface StoredObject { interface Store { name: string; keyPath: string | string[]; + keyGenerator: number; + autoIncrement: boolean; objects: { [strKey: string]: StoredObject }; } @@ -46,18 +58,585 @@ interface Databases { } +/** + * 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]; + } +} + + +function callEventHandler(h: EventListenerOrEventListenerObject, evt: Event, target: any) { + if ("handleEvent" in h) { + (h as EventListenerObject).handleEvent(evt); + } else { + (h as EventListener).call(target, evt); + } +} + +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; + + constructor(public _transaction: Transaction, public runner: () => void) { + } + + callSuccess(ev: Event) { + 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 null; + } + + get source() { + // buggy type definitions don't allow null even though it's in + // the spec. + return (null as any) 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); + } + } +} + + +class MyObjectStore implements IDBObjectStore { + get indexNames() { + return new DOMStringList(); + } + + constructor(public transaction: Transaction, public dbName: string, public storeName: string) { + } + + get keyPath() { + return this.transaction.db.dbData.stores[this.storeName].keyPath; + } + + get name() { + return this.storeName; + } + + get autoIncrement() { + return this.transaction.db.dbData.stores[this.storeName].autoIncrement; + } + + add(value: any, key?: any): IDBRequest { + throw Error("not implemented"); + } + + put(value: any, key?: any): IDBRequest { + throw Error("not implemented"); + } + + delete(key: any): IDBRequest { + throw Error("not implemented"); + } + + get(key: any): IDBRequest { + throw Error("not implemented"); + } + + 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 { + throw Error("not implemented"); + } + + 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; + + constructor(private _name: string, private _version: number, private factory: MemoryIDBFactory) { + } + + get dbData() { + return this.factory.data[this._name]; + } + + get name() { + return this._name; + } + + get objectStoreNames() { + return new MyDomStringList(); + } + + 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"); + } + throw Error("not implemented"); + } + + deleteObjectStore(name: string): void { + throw Error("not implemented"); + } + + 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; + } + + start() { + if (this.state != TransactionState.Created) { + throw Error(); + } + this._transactionDbData = structuredClone(this.dbHandle.dbData); + } + + 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, this.dbName, 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) { + this._type = typeArg; + } + + 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) { + super("VersionChange"); + this._oldVersion = oldVersion; + this._newVersion = newVersion || null; + } + + get newVersion() { + return this._newVersion; + } + + get oldVersion() { + return this._oldVersion; + } +} + + 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; + this.data[lastTx.dbName] = lastTx.transactionDbData; + } + }; + 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 { - return 0; + throw Error("not implemented"); } deleteDatabase(name: string): IDBOpenDBRequest { throw Error("not implemented"); } - open(name: string, version?: number): IDBOpenDBRequest { - throw Error("not implemented"); + open(dbName: string, version?: number): IDBOpenDBRequest { + if (version !== undefined && version <= 0) { + throw Error("invalid version"); + } + + let upgradeNeeded = false; + let mydb: Database; + if (dbName in this.data) { + mydb = this.data[dbName]; + if (version === undefined || version == mydb.version) { + // we can open without upgrading + } else if (version > mydb.version) { + upgradeNeeded = true; + } else { + throw Error("version error"); + } + } else { + mydb = { + name: dbName, + stores: {}, + version: (version || 1), + }; + upgradeNeeded = true; + } + + const db = new Db(dbName, mydb.version, this); + const tx = new Transaction(dbName, db, "versionchange"); + + const req = new OpenDBRequest(tx, () => { + if (upgradeNeeded) { + let versionChangeEvt = new VersionChangeEvent(mydb.version, version); + req.callOnupgradeneeded(versionChangeEvt); + } + let successEvent = new MyEvent("success"); + req.callSuccess(successEvent); + }); + + this.addRequest(req); + + return req; } } diff --git a/tsconfig.json b/tsconfig.json index 2ba62b510..ef56fdd22 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -41,6 +41,10 @@ "src/i18n/strings.ts", "src/logging.ts", "src/memidb.ts", + "src/node_modules/structured-clone/clone.js", + "src/node_modules/structured-clone/index.js", + "src/node_modules/structured-clone/serialize.js", + "src/node_modules/structured-clone/test/test.js", "src/query.ts", "src/timer.ts", "src/types-test.ts", |