Skip to content

Commit

Permalink
Fix db transaction errors (#176)
Browse files Browse the repository at this point in the history
* use sync catches to propagate errors correctly

* add retry logic in utils

* add variance to popup

* pull fixes from idb branch

* fix toast, fix queue import indent, user api vs chrome for send message

* don't queue safelisted users

* fix showing dates on chrome

* return promise

* changelog
  • Loading branch information
kheina authored Jul 4, 2023
1 parent af9503a commit f6db656
Show file tree
Hide file tree
Showing 18 changed files with 379 additions and 250 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ Summary
5. version timestamp follow the yyyy.MM.dd format
```

# v0.3.4 [2023.07.04]
- feat: show alert when user is logged out
- patch: write to history when block fails due to account deletion
- fix: retry after db failure
- fix: errors not surfacing during db transactions
- chore: add typedefs to all messages
- chore: display variance in popup

# v0.3.3 [2023.07.01]
- feat: track block history, click blocked number in context menu to access (#63)
- feat: added safelist control buttons: import, export, clear
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "blue-blocker",
"version": "0.3.3",
"version": "0.3.4",
"author": "DanielleMiu",
"description": "Blocks all Twitter Blue verified users on twitter.com",
"type": "module",
Expand Down
105 changes: 54 additions & 51 deletions src/background/db.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { api, logstr, EventKey, LegacyVerifiedUrl, MessageEvent, ErrorEvent, HistoryStateBlocked, HistoryStateUnblocked } from "../constants";
import { api, logstr, EventKey, LegacyVerifiedUrl, MessageEvent, ErrorEvent, HistoryStateUnblocked } from "../constants";
import { commafy } from "../utilities";

const expectedVerifiedUsersCount = 407520;
Expand Down Expand Up @@ -71,7 +71,7 @@ export async function PopulateVerifiedDb() {
});
}
catch (_e) {
const e = _e as Error;
// const e = _e as Error;
await new Promise<void>((resolve, reject) => {
const transaction = legacyDb.transaction([legacyDbStore], "readwrite");
const store = transaction.objectStore(legacyDbStore);
Expand Down Expand Up @@ -161,6 +161,7 @@ export async function PopulateVerifiedDb() {
}

export function CheckDbIsUserLegacyVerified(user_id: string, handle: string): Promise<boolean> {
// @ts-ignore // typescript is wrong here, this cannot return idb due to final throws
return new Promise<boolean>((resolve, reject) => {
const transaction = legacyDb.transaction([legacyDbStore], "readonly");
transaction.onabort = transaction.onerror = reject;
Expand All @@ -175,86 +176,84 @@ export function CheckDbIsUserLegacyVerified(user_id: string, handle: string): Pr
}).catch(e => {
// if the db has already been loaded, we can safely reconnect
if (legacyDbLoaded) {
PopulateVerifiedDb();
return PopulateVerifiedDb()
.finally(() => {
throw e; // re-throw error
});
}
throw e; // re-throw error
});
}

let historyDb: IDBDatabase;

export interface BlockedUser {
user_id: string,
user: { name: string, screen_name: string },
reason: number,
external_reason?: string,
state: number,
time: Date,
}
let db: IDBDatabase;

const historyDbName = "blue-blocker-db";
const dbName = "blue-blocker-db";
export const historyDbStore = "blocked_users";
const historyDbVersion = 1;
const dbVersion = 1;
// used so we don't load the db twice
let dbLoaded: boolean = false;

export function ConnectHistoryDb(): Promise<IDBDatabase> {
export function ConnectDb(): Promise<IDBDatabase> {
// why use a promise instead of a normal async? so that we can resolve or reject on db connect
return new Promise<IDBDatabase>(async (resolve, reject) => {
return new Promise<IDBDatabase>((resolve, reject) => {
// this logic should also be much easier because we don't need to populate anything (thank god)
const DBOpenRequest = indexedDB.open(historyDbName, historyDbVersion);
const DBOpenRequest = indexedDB.open(dbName, dbVersion);
DBOpenRequest.onerror = DBOpenRequest.onblocked = () => {
console.error(logstr, "failed to connect history database:", DBOpenRequest);
console.error(logstr, "failed to connect database:", DBOpenRequest);
return reject();
};

DBOpenRequest.onupgradeneeded = () => {
console.debug(logstr, "upgrading history db:", DBOpenRequest);
historyDb = DBOpenRequest.result;

if (historyDb.objectStoreNames.contains(historyDbStore)) {
return;
console.debug(logstr, "upgrading db:", DBOpenRequest);
db = DBOpenRequest.result;

if (!db.objectStoreNames.contains(historyDbStore)) {
const store = db.createObjectStore(historyDbStore, { keyPath: "user_id" });
store.createIndex("user.name", "user.name", { unique: false });
store.createIndex("user.screen_name", "user.screen_name", { unique: false });
store.createIndex("time", "time", { unique: false });
console.log(logstr, "created history database.");
}

const store = historyDb.createObjectStore(historyDbStore, { keyPath: "user_id" });
store.createIndex("user.name", "user.name", { unique: false });
store.createIndex("user.screen_name", "user.screen_name", { unique: false });
store.createIndex("time", "time", { unique: false });
console.log(logstr, "created history database.");
};

DBOpenRequest.onsuccess = () => {
historyDb = DBOpenRequest.result;
console.log(logstr, "successfully connected to history db");
return resolve(historyDb);
DBOpenRequest.onsuccess = async () => {
db = DBOpenRequest.result;
if (dbLoaded) {
return resolve(db);
}
dbLoaded = true;

console.log(logstr, "successfully connected to db");
return resolve(db);
};
});
}

export function AddUserToHistory(blockUser: BlockUser): Promise<void> {
const user: BlockedUser = {
...blockUser,
state: HistoryStateBlocked,
time: new Date(),
};
return new Promise<void>(async (resolve, reject) => {
const transaction = historyDb.transaction([historyDbStore], "readwrite");
export function AddUserToHistory(user: BlockedUser): Promise<void> {
// @ts-ignore // typescript is wrong here, this cannot return idb due to final throw
return new Promise<void>((resolve, reject) => {
const transaction = db.transaction([historyDbStore], "readwrite");
transaction.onabort = transaction.onerror = reject;
transaction.oncomplete = () => resolve();

const store = transaction.objectStore(historyDbStore);
store.add(user);

transaction.commit();
}).catch(async (e) => {
}).catch(e =>
// attempt to reconnect to the db
await ConnectHistoryDb();
throw e; // re-throw error to retry
});
ConnectDb()
.finally(() => {
throw e; // re-throw error to retry
})
);
}

export function RemoveUserFromHistory(user_id: string): Promise<void> {
// @ts-ignore // typescript is wrong here, this cannot return idb due to final throw
return new Promise<void>(async (resolve, reject) => {
try {
const transaction = historyDb.transaction([historyDbStore], "readwrite");
const transaction = db.transaction([historyDbStore], "readwrite");
transaction.onabort = transaction.onerror = reject;
transaction.oncomplete = () => resolve();

Expand All @@ -266,6 +265,8 @@ export function RemoveUserFromHistory(user_id: string): Promise<void> {
const user = req.result as BlockedUser;
res(user);
};
}).catch(e => {
throw e;
});

user.state = HistoryStateUnblocked;
Expand All @@ -276,9 +277,11 @@ export function RemoveUserFromHistory(user_id: string): Promise<void> {
} catch (e) {
reject(e);
}
}).catch(async (e) => {
}).catch(e =>
// attempt to reconnect to the db
await ConnectHistoryDb();
throw e; // re-throw error to retry
});
ConnectDb()
.finally(() => {
throw e; // re-throw error to retry
})
);
}
116 changes: 50 additions & 66 deletions src/background/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { api, logstr, AddToHistoryAction, ErrorStatus, IsVerifiedAction, MessageStatus, ReasonExternal, RemoveFromHistoryAction, SoupcanExtensionId, SuccessStatus, DefaultOptions } from '../constants';
import { abbreviate } from '../utilities';
import { AddUserToHistory, CheckDbIsUserLegacyVerified, ConnectHistoryDb, PopulateVerifiedDb, RemoveUserFromHistory } from './db';
import { api, logstr, AddToHistoryAction, ErrorStatus, IsVerifiedAction, ReasonExternal, RemoveFromHistoryAction, SoupcanExtensionId, SuccessStatus, DefaultOptions } from '../constants';
import { abbreviate, RefId } from '../utilities';
import { AddUserToHistory, CheckDbIsUserLegacyVerified, ConnectDb, PopulateVerifiedDb, RemoveUserFromHistory } from './db';
import { BlockQueue } from '../models/block_queue';

api.action.setBadgeBackgroundColor({ color: "#666" });
Expand Down Expand Up @@ -37,74 +37,56 @@ api.storage.sync.onChanged.addListener(async items => {
}
});

ConnectHistoryDb();
ConnectDb();

interface Response {
status: MessageStatus,
}

interface SuccessResponse {
status: "SUCCESS",
result: any,
}

interface ErrorResponse {
status: "ERROR",
message: string,
error?: Error,
}

api.runtime.onMessage.addListener((m, s, r) => { (async (_message, sender, respond) => {
api.runtime.onMessage.addListener((m, s, r) => { let response: MessageResponse; (async (message: RuntimeMessage, sender) => {
const refid = RefId();
console.debug(logstr, refid, "recv:", message, sender);
// messages are ALWAYS expected to be:
// 1. objects
// 2. contain a string value stored under message.action. should be one defined above
// other message contents change based on the defined action
let response: Response;
switch (_message?.action) {
case IsVerifiedAction:
const verifiedMessage = _message as { user_id: string, handle: string };
try {
try {
switch (message?.action) {
case IsVerifiedAction:
const verifiedMessage = message.data as { user_id: string, handle: string };
const isVerified = await CheckDbIsUserLegacyVerified(verifiedMessage.user_id, verifiedMessage.handle);
response = { status: SuccessStatus, result: isVerified } as SuccessResponse;
} catch (e) {
response = { status: ErrorStatus, message: "unknown error", error: e } as ErrorResponse;
}
break;
break;

case AddToHistoryAction:
const historyMessage = _message.data as BlockUser;
try {
case AddToHistoryAction:
const historyMessage = message.data as BlockedUser;
await AddUserToHistory(historyMessage);
response = { status: SuccessStatus, result: null } as SuccessResponse;
} catch (e) {
response = { status: ErrorStatus, message: "unknown error", error: e } as ErrorResponse;
}
break;
break;

case RemoveFromHistoryAction:
const removeMessage = _message.data as { user_id: string };
try {
case RemoveFromHistoryAction:
const removeMessage = message.data as { user_id: string };
await RemoveUserFromHistory(removeMessage.user_id);
response = { status: SuccessStatus, result: null } as SuccessResponse;
} catch (e) {
response = { status: ErrorStatus, message: "unknown error", error: e } as ErrorResponse;
}
break;

default:
console.error(logstr, "got a message that couldn't be handled from sender:", sender, _message);
response = { status: ErrorStatus, message: "unknown action" } as ErrorResponse;
break;

default:
console.error(logstr, refid, "got a message that couldn't be handled from sender:", sender, message);
response = { status: ErrorStatus, message: "unknown action" } as ErrorResponse;
}
} catch (_e) {
const e = _e as Error;
console.error(logstr, refid, "unexpected error caught during", message?.action, "action", e);
response = { status: ErrorStatus, message: e.message ?? "unknown error" } as ErrorResponse;
}
respond(response);
})(m, s, r); return true });
console.debug(logstr, refid, "respond:", response);
})(m, s).finally(() => r(response)); return true });

////////////////////////////////////////////////// EXTERNAL MESSAGE HANDLING //////////////////////////////////////////////////

const queue = new BlockQueue(api.storage.local);
const [blockAction] = ["BLOCK"];
const allowedExtensionIds = new Set([SoupcanExtensionId]);

api.runtime.onMessageExternal.addListener((m, s, r) => { (async (_message, sender, respond) => {
api.runtime.onMessageExternal.addListener((m, s, r) => { let response: MessageResponse; (async (message, sender) => {
const refid = RefId();
console.debug(logstr, refid, "ext recv:", message, sender);
if (!allowedExtensionIds.has(sender?.id ?? "")) {
return;
}
Expand All @@ -113,21 +95,23 @@ api.runtime.onMessageExternal.addListener((m, s, r) => { (async (_message, sende
// 1. objects
// 2. contain a string value stored under message.action. should be one defined above
// other message contents change based on the defined action
let response: Response;
switch (_message?.action) {
case blockAction:
const message = _message as { action: string, user_id: string, name: string, screen_name: string, reason: string };
try {
await queue.push({ user_id: message.user_id, user: { name: message.name, screen_name: message.screen_name }, reason: ReasonExternal, external_reason: message.reason });
try {
switch (message?.action) {
case blockAction:
const blockMessage = message as { user_id: string, name: string, screen_name: string, reason: string };
const user: BlockUser = { user_id: blockMessage.user_id, user: { name: blockMessage.name, screen_name: blockMessage.screen_name }, reason: ReasonExternal, external_reason: blockMessage.reason };
await queue.push(user);
response = { status: SuccessStatus, result: "user queued for blocking" } as SuccessResponse;
} catch (e) {
response = { status: ErrorStatus, message: "unknown error", error: e } as ErrorResponse;
}
break;

default:
console.error(logstr, "got a message that couldn't be handled from sender:", sender, _message);
response = { status: ErrorStatus, message: "unknown action" } as ErrorResponse;
break;

default:
console.error(logstr, refid, "got a message that couldn't be handled from sender:", sender, message);
response = { status: ErrorStatus, message: "unknown action" } as ErrorResponse;
}
} catch (_e) {
const e = _e as Error;
console.error(logstr, refid, "unexpected error caught during", message?.action, "action", e);
response = { status: ErrorStatus, message: e.message ?? "unknown error" } as ErrorResponse;
}
respond(response);
})(m, s, r); return true });
console.debug(logstr, refid, "respond:", response);
})(m, s).finally(() => r(response)); return true });
Loading

0 comments on commit f6db656

Please sign in to comment.