Skip to content

Commit

Permalink
Fix possible race conditions
Browse files Browse the repository at this point in the history
  • Loading branch information
riccardobl committed Jan 17, 2024
1 parent 104c046 commit f89dcd6
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 56 deletions.
76 changes: 44 additions & 32 deletions src/js/AssetProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ export default class AssetProvider {
try {
this.starting = true;
// restore tracked assets
this.ready = true;

const trackedAssets = await this.store.get("trackedAssets");
if (trackedAssets) {
Expand All @@ -102,6 +101,7 @@ export default class AssetProvider {

this.staticIcons = Icons;
this.specialSymbols = SpecialSymbols;
this.ready = true;
} catch (e) {
console.error(e);
} finally {
Expand Down Expand Up @@ -198,45 +198,57 @@ export default class AssetProvider {
async track(assetHash, noInit = false) {
if (!noInit) await this._init();
if (assetHash === this.baseAssetId) return;
if (this._isFiat(assetHash)) {
if (this.trackedFiatAssets.indexOf(assetHash) < 0) {
this.trackedFiatAssets.push(assetHash);
await this.store.set("trackedFiatAssets", this.trackedFiatAssets);
await this.store.lock("trackedAssets");
try {
console.log("Track", assetHash);

if (this._isFiat(assetHash)) {
if (this.trackedFiatAssets.indexOf(assetHash) < 0) {
this.trackedFiatAssets.push(assetHash);
await this.store.set("trackedFiatAssets", this.trackedFiatAssets);
}
return;
}
return;
}

if (this.trackedAssets.indexOf(assetHash) >= 0) return;
if (this.trackedAssets.indexOf(assetHash) >= 0) return;

this.trackedAssets.push(assetHash);
await this.store.set("trackedAssets", this.trackedAssets);
this.trackedAssets.push(assetHash);
await this.store.set("trackedAssets", this.trackedAssets);

let first = true;
return new Promise((res, rej) => {
const trackerCallback = async (price, baseAssetId) => {
await this.cache.set("p:" + assetHash, price);
if (first) {
res(price);
first = false;
}
};
if (!this.trackerCallbacks) this.trackerCallbacks = {};
this.trackerCallbacks[assetHash] = trackerCallback;
this.sideSwap.subscribeToAssetPriceUpdate(assetHash, trackerCallback);
});
let first = true;
return new Promise((res, rej) => {
const trackerCallback = async (price, baseAssetId) => {
await this.cache.set("p:" + assetHash, price);
if (first) {
res(price);
first = false;
}
};
if (!this.trackerCallbacks) this.trackerCallbacks = {};
this.trackerCallbacks[assetHash] = trackerCallback;
this.sideSwap.subscribeToAssetPriceUpdate(assetHash, trackerCallback);
});
} finally {
await this.store.unlock("trackedAssets");
}
}

async untrack(assetHash) {
if (assetHash === this.baseAssetId) return;
if (this._isFiat(assetHash)) return;
const index = this.trackedAssets.indexOf(assetHash);
if (index < 0) return;
this.trackedAssets.splice(index, 1);
await this.store.set("trackedAssets", this.trackedAssets);
const trackerCallback = this.trackerCallbacks[assetHash];
if (trackerCallback) {
this.sideSwap.unsubscribeFromAssetPriceUpdate(assetHash, trackerCallback);
delete this.trackerCallbacks[assetHash];
await this.store.lock("trackedAssets");
try {
if (this._isFiat(assetHash)) return;
const index = this.trackedAssets.indexOf(assetHash);
if (index < 0) return;
this.trackedAssets.splice(index, 1);
await this.store.set("trackedAssets", this.trackedAssets);
const trackerCallback = this.trackerCallbacks[assetHash];
if (trackerCallback) {
this.sideSwap.unsubscribeFromAssetPriceUpdate(assetHash, trackerCallback);
delete this.trackerCallbacks[assetHash];
}
} finally {
await this.store.unlock("trackedAssets");
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/js/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default {
HARDCODED_FEE: 0.27, // used when the fee api fails
DEBOUNCE_CALLBACK_TIME: 500, // lower this value to make the app more responsive, but more resource intensive
APP_ID: "lq", // The app id
APP_VERSION: 1, // bumping this invalidates all cache and storage
APP_VERSION: 10, // bumping this invalidates all cache and storage
STORAGE_METHODS: ["LocalStore", "IDBStore", "MemStore"], // order matters, first is preferred
STORAGE_METHODS_BY_SPEED: ["LocalStore", "IDBStore", "MemStore"], // order matters, first is fastest

Expand Down
4 changes: 2 additions & 2 deletions src/js/LiquidWallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -1409,7 +1409,7 @@ export default class LiquidWallet {
*/
async pinAsset(assetHash) {
await this.check();
this.assetProvider.track(assetHash);
return this.assetProvider.track(assetHash);
}

/**
Expand All @@ -1418,7 +1418,7 @@ export default class LiquidWallet {
*/
async unpinAsset(assetHash) {
await this.check();
this.assetProvider.untrack(assetHash);
return this.assetProvider.untrack(assetHash);
}

/**
Expand Down
83 changes: 62 additions & 21 deletions src/js/storage/AbstractBrowserStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ export default class AbstractBrowserStore {
constructor(prefix, limit) {
this.limit = limit;
this.prefix = prefix;
this.locks = {};
}

async lock(key) {
let t = 1;
while (this.locks[key]) {
await new Promise((resolve) => setTimeout(resolve, t));
t *= 2;
if (t > 100) t = 100;
}
this.locks[key] = true;
}

unlock(key) {
delete this.locks[key];
}

async _init() {
Expand All @@ -21,9 +36,9 @@ export default class AbstractBrowserStore {
this.starting = true;

this.accessTable = await this._retrieve("s:accessTable");

this.expirationTable = await this._retrieve("s:expirationTable");
this.sizeTable = await this._retrieve("s:sizeTable");

if (!this.accessTable) {
this.accessTable = new Map();
}
Expand Down Expand Up @@ -79,9 +94,9 @@ export default class AbstractBrowserStore {

if (!value) {
await this._delete(key);
this.accessTable.delete(key);
this.expirationTable.delete(key);
this.sizeTable.delete(key);
await this._setAccessTime(key, undefined);
await this._setExpiration(key, undefined);
await this._setSize(key, undefined);
} else {
const entrySize = await this._calcSize(key, value);
if (this.limit) {
Expand All @@ -90,26 +105,51 @@ export default class AbstractBrowserStore {
}
}
await this._store(key, value);
// localStorage.setItem(key, JSON.stringify(value));
this.accessTable.set(key, Date.now());
if (expiration) this.expirationTable.set(key, Date.now() + expiration);
await this._setAccessTime(key, Date.now());
await this._setExpiration(key, expiration ? Date.now() + expiration : undefined);
await this._setSize(key, entrySize);
}
}

async _setAccessTime(key, time) {
await this._init();
await this.lock("s:");
try {
if (time) this.accessTable.set(key, time);
else this.accessTable.delete(key);
await this._store("s:accessTable", this.accessTable);
} finally {
this.unlock("s:");
}
}

async _setExpiration(key, time) {
await this._init();
await this.lock("s:");
try {
if (time) this.expirationTable.set(key, time);
else this.expirationTable.delete(key);
this.sizeTable.set(key, entrySize);
await this._store("s:expirationTable", this.expirationTable);
} finally {
this.unlock("s:");
}
this._store("s:accessTable", this.accessTable);
this._store("s:expirationTable", this.expirationTable);
this._store("s:sizeTable", this.sizeTable);
// localStorage.setItem('accessTable', JSON.stringify(Array.from(this.accessTable.entries())));
// localStorage.setItem('expirationTable', JSON.stringify(Array.from(this.expirationTable.entries())));
// localStorage.setItem('sizeTable', JSON.stringify(Array.from(this.sizeTable.entries())));
}

async clear() {
async _setSize(key, size) {
await this._init();
await this.lock("s:");
try {
if (size) this.sizeTable.set(key, size);
else this.sizeTable.delete(key);
await this._store("s:sizeTable", this.sizeTable);
} finally {
this.unlock("s:");
}
}

async clear() {
await this._init();
const keys = this.accessTable.keys();
console.log(keys);

for (const key of keys) {
if (key.startsWith("s:")) continue;
console.log("Clearing " + key);
Expand All @@ -123,21 +163,23 @@ export default class AbstractBrowserStore {
if (key.startsWith("s:")) throw new Error("Key cannot start with s:");

let value;
let exists = this.accessTable.has(key) && this.sizeTable.has(key); // corrupted data
let exists = this.accessTable.has(key) && this.sizeTable.has(key);
if (!exists) {
// corrupted data
console.log("Corrupted data for " + key);
await this.set(key, undefined);
value = undefined;
} else {
value = await this._retrieve(key, asDataUrl);
}

if (value) {
this.accessTable.set(key, Date.now());
await this._store("s:accessTable", this.accessTable);
await this._setAccessTime(key, Date.now());
}

const expire = this.expirationTable.get(key);
if (!value || (expire && expire < Date.now())) {
console.log("Refreshing " + key);
if (refreshCallback) {
let refreshed = Promise.resolve(refreshCallback());
refreshed = refreshed.then(async (data) => {
Expand Down Expand Up @@ -170,7 +212,6 @@ export default class AbstractBrowserStore {
for (let [key, access] of this.accessTable) {
if (key.startsWith("s:")) continue;
if (access < oldestAccess) {
alert("Deleting " + key);
oldestAccess = access;
oldestKey = key;
}
Expand Down

0 comments on commit f89dcd6

Please sign in to comment.