aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/timer.ts66
-rw-r--r--src/wallet.ts40
-rw-r--r--src/webex/pages/popup.tsx2
-rw-r--r--src/webex/wxBackend.ts72
4 files changed, 139 insertions, 41 deletions
diff --git a/src/timer.ts b/src/timer.ts
index dd67dbd29..f19e975ac 100644
--- a/src/timer.ts
+++ b/src/timer.ts
@@ -75,3 +75,69 @@ export function every(delayMs: number, callback: () => void): TimerHandle {
export function after(delayMs: number, callback: () => void): TimerHandle {
return new TimeoutHandle(setInterval(callback, delayMs));
}
+
+
+const nullTimerHandle = {
+ clear() {
+ }
+};
+
+/**
+ * Group of timers that can be destroyed at once.
+ */
+export class TimerGroup {
+ private stopped: boolean = false;
+
+ private timerMap: { [index: number]: TimerHandle } = {};
+
+ private idGen = 1;
+
+ stopCurrentAndFutureTimers() {
+ this.stopped = true;
+ for (const x in this.timerMap) {
+ if (!this.timerMap.hasOwnProperty(x)) {
+ continue;
+ }
+ this.timerMap[x].clear();
+ delete this.timerMap[x];
+ }
+ }
+
+ after(delayMs: number, callback: () => void): TimerHandle {
+ if (this.stopped) {
+ console.warn("dropping timer since timer group is stopped");
+ return nullTimerHandle;
+ }
+ const h = after(delayMs, callback);
+ let myId = this.idGen++;
+ this.timerMap[myId] = h;
+
+ const tm = this.timerMap;
+
+ return {
+ clear() {
+ h.clear();
+ delete tm[myId];
+ },
+ };
+ }
+
+ every(delayMs: number, callback: () => void): TimerHandle {
+ if (this.stopped) {
+ console.warn("dropping timer since timer group is stopped");
+ return nullTimerHandle;
+ }
+ const h = every(delayMs, callback);
+ let myId = this.idGen++;
+ this.timerMap[myId] = h;
+
+ const tm = this.timerMap;
+
+ return {
+ clear() {
+ h.clear();
+ delete tm[myId];
+ },
+ };
+ }
+}
diff --git a/src/wallet.ts b/src/wallet.ts
index 0d4a8e0dc..a4e1d46f3 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -43,6 +43,7 @@ import {
QueryRoot,
Store,
} from "./query";
+import {TimerGroup} from "./timer";
import {
AmountJson,
Amounts,
@@ -346,18 +347,6 @@ const builtinCurrencies: CurrencyRecord[] = [
];
-// FIXME: these functions should be dependency-injected
-// into the wallet, as this is chrome specific => bad
-
-function setTimeout(f: any, t: number) {
- return chrome.extension.getBackgroundPage().setTimeout(f, t);
-}
-
-function setInterval(f: any, t: number) {
- return chrome.extension.getBackgroundPage().setInterval(f, t);
-}
-
-
function isWithdrawableDenom(d: DenominationRecord) {
const nowSec = (new Date()).getTime() / 1000;
const stampWithdrawSec = getTalerStampSec(d.stampExpireWithdraw);
@@ -583,13 +572,17 @@ interface CoinsForPaymentArgs {
* The platform-independent wallet implementation.
*/
export class Wallet {
- private db: IDBDatabase;
+ /**
+ * IndexedDB database used by the wallet.
+ */
+ db: IDBDatabase;
private http: HttpRequestLibrary;
private badge: Badge;
private notifier: Notifier;
private cryptoApi: CryptoApi;
private processPreCoinConcurrent = 0;
private processPreCoinThrottle: {[url: string]: number} = {};
+ private timerGroup: TimerGroup;
/**
* Set of identifiers for running operations.
@@ -613,7 +606,9 @@ export class Wallet {
this.fillDefaults();
this.resumePendingFromDb();
- setInterval(() => this.updateExchanges(), 1000 * 60 * 15);
+ this.timerGroup = new TimerGroup();
+
+ this.timerGroup.every(1000 * 60 * 15, () => this.updateExchanges());
}
private async fillDefaults() {
@@ -1027,8 +1022,7 @@ export class Wallet {
// random, exponential backoff truncated at 3 minutes
const nextDelay = Math.min(2 * retryDelayMs + retryDelayMs * Math.random(), 3000 * 60);
console.warn(`Failed to deplete reserve, trying again in ${retryDelayMs} ms`);
- setTimeout(() => this.processReserve(reserveRecord, nextDelay),
- retryDelayMs);
+ this.timerGroup.after(retryDelayMs, () => this.processReserve(reserveRecord, nextDelay))
} finally {
this.stopOperation(opId);
}
@@ -1039,8 +1033,7 @@ export class Wallet {
retryDelayMs = 200): Promise<void> {
if (this.processPreCoinConcurrent >= 4 || this.processPreCoinThrottle[preCoin.exchangeBaseUrl]) {
console.log("delaying processPreCoin");
- setTimeout(() => this.processPreCoin(preCoin, Math.min(retryDelayMs * 2, 5 * 60 * 1000)),
- retryDelayMs);
+ this.timerGroup.after(retryDelayMs, () => this.processPreCoin(preCoin, Math.min(retryDelayMs * 2, 5 * 60 * 1000)));
return;
}
console.log("executing processPreCoin");
@@ -1098,12 +1091,11 @@ export class Wallet {
"ms", e);
// exponential backoff truncated at one minute
const nextRetryDelayMs = Math.min(retryDelayMs * 2, 5 * 60 * 1000);
- setTimeout(() => this.processPreCoin(preCoin, nextRetryDelayMs),
- retryDelayMs);
+ this.timerGroup.after(retryDelayMs, () => this.processPreCoin(preCoin, nextRetryDelayMs))
const currentThrottle = this.processPreCoinThrottle[preCoin.exchangeBaseUrl] || 0;
this.processPreCoinThrottle[preCoin.exchangeBaseUrl] = currentThrottle + 1;
- setTimeout(() => {this.processPreCoinThrottle[preCoin.exchangeBaseUrl]--; }, retryDelayMs);
+ this.timerGroup.after(retryDelayMs, () => {this.processPreCoinThrottle[preCoin.exchangeBaseUrl]--; });
} finally {
this.processPreCoinConcurrent--;
}
@@ -2335,4 +2327,10 @@ export class Wallet {
return await this.q().iter(Stores.reserves).filter((r) => r.hasPayback).toArray();
}
+ /**
+ * Stop ongoing processing.
+ */
+ stop() {
+ this.timerGroup.stopCurrentAndFutureTimers();
+ }
}
diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx
index d7429f837..831147f1e 100644
--- a/src/webex/pages/popup.tsx
+++ b/src/webex/pages/popup.tsx
@@ -526,7 +526,7 @@ function openExtensionPage(page: string) {
function openTab(page: string) {
- return (evt) => {
+ return (evt: React.SyntheticEvent<any>) => {
evt.preventDefault();
chrome.tabs.create({
url: page,
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 30f127347..35fa0b573 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -246,9 +246,9 @@ function handleMessage(db: IDBDatabase,
}
}
-async function dispatch(db: IDBDatabase, wallet: Wallet, req: any, sender: any, sendResponse: any): Promise<void> {
+async function dispatch(wallet: Wallet, req: any, sender: any, sendResponse: any): Promise<void> {
try {
- const p = handleMessage(db, wallet, sender, req.type, req.detail);
+ const p = handleMessage(wallet.db, wallet, sender, req.type, req.detail);
const r = await p;
try {
sendResponse(r);
@@ -421,6 +421,38 @@ function clearRateLimitCache() {
rateLimitCache = {};
}
+
+/**
+ * Currently active wallet instance. Might be unloaded and
+ * re-instantiated when the database is reset.
+ */
+let currentWallet: Wallet|undefined;
+
+
+async function reinitWallet() {
+ if (currentWallet) {
+ currentWallet.stop();
+ currentWallet = undefined;
+ }
+ chrome.browserAction.setBadgeText({ text: "" });
+ const badge = new ChromeBadge();
+ let db: IDBDatabase;
+ try {
+ db = await openTalerDb();
+ } catch (e) {
+ console.error("could not open database", e);
+ return;
+ }
+ const http = new BrowserHttpLib();
+ const notifier = new ChromeNotifier();
+ console.log("setting wallet");
+ const wallet = new Wallet(db, http, badge, notifier);
+ // Useful for debugging in the background page.
+ (window as any).talerWallet = wallet;
+ currentWallet = wallet;
+}
+
+
/**
* Main function to run for the WebExtension backend.
*
@@ -438,9 +470,6 @@ export async function wxMain() {
logging.record("error", m + error, undefined, source || "(unknown)", lineno || 0, colno || 0);
};
- chrome.browserAction.setBadgeText({ text: "" });
- const badge = new ChromeBadge();
-
chrome.tabs.query({}, (tabs) => {
for (const tab of tabs) {
if (!tab.url || !tab.id) {
@@ -514,29 +543,28 @@ export async function wxMain() {
chrome.extension.getBackgroundPage().setInterval(clearRateLimitCache, 5000);
- let db: IDBDatabase;
- try {
- db = await openTalerDb();
- } catch (e) {
- console.error("could not open database", e);
- return;
- }
- const http = new BrowserHttpLib();
- const notifier = new ChromeNotifier();
- console.log("setting wallet");
- const wallet = new Wallet(db, http, badge!, notifier);
- // Useful for debugging in the background page.
- (window as any).talerWallet = wallet;
+ reinitWallet();
// Handlers for messages coming directly from the content
// script on the page
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
- dispatch(db, wallet, req, sender, sendResponse);
+ const wallet = currentWallet;
+ if (!wallet) {
+ console.warn("wallet not available while handling message");
+ console.warn("dropped request message was", req);
+ return;
+ }
+ dispatch(wallet, req, sender, sendResponse);
return true;
});
+
// Handlers for catching HTTP requests
chrome.webRequest.onHeadersReceived.addListener((details) => {
+ const wallet = currentWallet;
+ if (!wallet) {
+ console.warn("wallet not available while handling header");
+ }
if (details.statusCode === 402) {
console.log(`got 402 from ${details.url}`);
return handleHttpPayment(details.responseHeaders || [],
@@ -559,9 +587,15 @@ function openTalerDb(): Promise<IDBDatabase> {
return new Promise<IDBDatabase>((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onerror = (e) => {
+ console.log("taler database error", e);
reject(e);
};
req.onsuccess = (e) => {
+ req.result.onversionchange = (evt: IDBVersionChangeEvent) => {
+ console.log(`handling live db version change from ${evt.oldVersion} to ${evt.newVersion}`);
+ req.result.close();
+ reinitWallet();
+ };
resolve(req.result);
};
req.onupgradeneeded = (e) => {