/*
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
*/
/**
* 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 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);
}
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;
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 this._result;
}
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);
}
}
}
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");
}
store.objects[stringKey] = value;
const req = new MyRequest(this.transaction, () => {
});
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 {
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 {
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);
}
let successEvent = new MyEvent("success", db);
req.callSuccess(successEvent);
});
this.addRequest(req);
return req;
}
}
/**
* Inject our IndexedDb implementation in the global namespace,
* potentially replacing an existing implementation.
*/
export function injectGlobals() {
}