diff options
-rw-r--r-- | src/timer.ts | 66 | ||||
-rw-r--r-- | src/wallet.ts | 40 | ||||
-rw-r--r-- | src/webex/pages/popup.tsx | 2 | ||||
-rw-r--r-- | src/webex/wxBackend.ts | 72 |
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) => { |