Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
IndexedDB: "resource://gre/modules/IndexedDB.sys.mjs",
ProfilesDatastoreService:
"moz-src:///toolkit/profile/ProfilesDatastoreService.sys.mjs",
ASRouterPreferences:
"resource:///modules/asrouter/ASRouterPreferences.sys.mjs",
});
export class ASRouterStorage {
/**
* @param storeNames Array of strings used to create all the required stores
*/
constructor({ storeNames, telemetry }) {
if (!storeNames) {
throw new Error("storeNames required");
}
this.dbName = "ActivityStream";
this.dbVersion = 3;
this.storeNames = storeNames;
this.telemetry = telemetry;
}
get db() {
return this._db || (this._db = this.createOrOpenDb());
}
/**
* Public method that binds the store required by the consumer and exposes
* the private db getters and setters.
*
* @param storeName String name of desired store
*/
getDbTable(storeName) {
if (this.storeNames.includes(storeName)) {
return {
get: this._get.bind(this, storeName),
getAll: this._getAll.bind(this, storeName),
getAllKeys: this._getAllKeys.bind(this, storeName),
set: this._set.bind(this, storeName),
getSharedMessageImpressions:
this.getSharedMessageImpressions.bind(this),
getSharedMessageBlocklist: this.getSharedMessageBlocklist.bind(this),
setSharedMessageImpressions:
this.setSharedMessageImpressions.bind(this),
setSharedMessageBlocked: this.setSharedMessageBlocked.bind(this),
};
}
throw new Error(`Store name ${storeName} does not exist.`);
}
async _getStore(storeName) {
return (await this.db).objectStore(storeName, "readwrite");
}
_get(storeName, key) {
return this._requestWrapper(async () =>
(await this._getStore(storeName)).get(key)
);
}
_getAll(storeName) {
return this._requestWrapper(async () =>
(await this._getStore(storeName)).getAll()
);
}
_getAllKeys(storeName) {
return this._requestWrapper(async () =>
(await this._getStore(storeName)).getAllKeys()
);
}
_set(storeName, key, value) {
return this._requestWrapper(async () =>
(await this._getStore(storeName)).put(value, key)
);
}
_openDatabase() {
return lazy.IndexedDB.open(this.dbName, this.dbVersion, db => {
// If provided with array of objectStore names we need to create all the
// individual stores
this.storeNames.forEach(store => {
if (!db.objectStoreNames.contains(store)) {
this._requestWrapper(() => db.createObjectStore(store));
}
});
});
}
/**
* createOrOpenDb - Open a db (with this.dbName) if it exists.
* If it does not exist, create it.
* If an error occurs, deleted the db and attempt to
* re-create it.
* @returns Promise that resolves with a db instance
*/
async createOrOpenDb() {
try {
const db = await this._openDatabase();
return db;
} catch (e) {
if (this.telemetry) {
this.telemetry.handleUndesiredEvent({ event: "INDEXEDDB_OPEN_FAILED" });
}
await lazy.IndexedDB.deleteDatabase(this.dbName);
return this._openDatabase();
}
}
async _requestWrapper(request) {
let result = null;
try {
result = await request();
} catch (e) {
if (this.telemetry) {
this.telemetry.handleUndesiredEvent({ event: "TRANSACTION_FAILED" });
}
throw e;
}
return result;
}
/**
* Gets all of the message impression data
* @returns {object|null} All multiprofile message impressions or null if error occurs
*/
async getSharedMessageImpressions() {
try {
const conn = await lazy.ProfilesDatastoreService.getConnection();
if (!conn) {
return null;
}
const rows = await conn.executeCached(
`SELECT messageId, json(impressions) AS impressions FROM MessagingSystemMessageImpressions;`
);
if (rows.length === 0) {
return null;
}
const impressionsData = {};
for (const row of rows) {
const messageId = row.getResultByName("messageId");
const impressions = JSON.parse(row.getResultByName("impressions"));
impressionsData[messageId] = impressions;
}
return impressionsData;
} catch (e) {
lazy.ASRouterPreferences.console.error(
`ASRouterStorage: Failed reading from MessagingSystemMessageImpressions`,
e
);
if (this.telemetry) {
this.telemetry.handleUndesiredEvent({
event: "SHARED_DB_READ_FAILED",
});
}
return null;
}
}
/**
* Gets the message blocklist
* @returns {Array|null} The message blocklist, or null if error occurred
*/
async getSharedMessageBlocklist() {
try {
const conn = await lazy.ProfilesDatastoreService.getConnection();
if (!conn) {
return null;
}
const rows = await conn.executeCached(
`SELECT messageId FROM MessagingSystemMessageBlocklist;`
);
return rows.map(row => row.getResultByName("messageId"));
} catch (e) {
lazy.ASRouterPreferences.console.error(
`ASRouterStorage: Failed reading from MessagingSystemMessageBlocklist`,
e
);
if (this.telemetry) {
this.telemetry.handleUndesiredEvent({
event: "SHARED_DB_READ_FAILED",
});
}
return null;
}
}
/**
* Set the message impressions for a given message ID
* @param {string} messageId - The message ID to set the impressions for
* @param {Array|null} impressions - The new value of "impressions" (an array of
* impression data or an emtpy array, or null to delete)
* @returns {boolean} Success status
*/
async setSharedMessageImpressions(messageId, impressions) {
let success = true;
try {
const conn = await lazy.ProfilesDatastoreService.getConnection();
if (!conn) {
return false;
}
if (!messageId) {
throw new Error(
"Failed attempt to set shared message impressions with no message ID."
);
}
// If impressions is falsy, delete the row (an empty array may indicate a custom
// frequency cap; we still want to track the message ID in that case.)
if (!impressions) {
await conn.executeBeforeShutdown(
"ASRouter: setSharedMessageImpressions",
async () => {
await conn.executeCached(
`DELETE FROM MessagingSystemMessageImpressions WHERE messageId = :messageId;`,
{
messageId,
}
);
}
);
} else {
await conn.executeBeforeShutdown(
"ASRouter: setSharedMessageImpressions",
async () => {
await conn.executeCached(
`INSERT INTO MessagingSystemMessageImpressions (messageId, impressions) VALUES (
:messageId,
jsonb(:impressions)
)
ON CONFLICT (messageId) DO UPDATE SET impressions = excluded.impressions;`,
{
messageId,
impressions: JSON.stringify(impressions),
}
);
}
);
}
lazy.ProfilesDatastoreService.notify();
} catch (e) {
lazy.ASRouterPreferences.console.error(
`ASRouterStorage: Failed writing to MessagingSystemMessageImpressions`,
e
);
if (this.telemetry) {
this.telemetry.handleUndesiredEvent({
event: "SHARED_DB_WRITE_FAILED",
});
}
success = false;
}
return success;
}
/**
* Adds a message ID to the blocklist and removes impressions
* for that message ID from the impressions table when isBlocked is true
* and deletes message ID from the blocklist when isBlocked is false
* @param {string} messageId - The message ID to set the blocked status for
* @param {boolean} [isBlocked=true] - If the message should be blocked (true) or unblocked (false)
* @returns {boolean} Success status
*/
async setSharedMessageBlocked(messageId, isBlocked = true) {
let success = true;
if (isBlocked) {
// Block the message, and clear impressions
try {
const conn = await lazy.ProfilesDatastoreService.getConnection();
if (!conn) {
return false;
}
await conn.executeTransaction(async () => {
await conn.executeCached(
`INSERT INTO MessagingSystemMessageBlocklist (messageId)
VALUES (:messageId);`,
{
messageId,
}
);
await conn.executeCached(
`DELETE FROM MessagingSystemMessageImpressions
WHERE messageId = :messageId;`,
{
messageId,
}
);
});
} catch (e) {
lazy.ASRouterPreferences.console.error(
`ASRouterStorage: Failed writing to MessagingSystemMessageBlocklist`,
e
);
if (this.telemetry) {
this.telemetry.handleUndesiredEvent({
event: "SHARED_DB_WRITE_FAILED",
});
}
success = false;
}
} else {
// Unblock the message
try {
const conn = await lazy.ProfilesDatastoreService.getConnection();
if (!conn) {
return false;
}
await conn.executeBeforeShutdown(
"ASRouter: setSharedMessageBlocked",
async () => {
await conn.executeCached(
`DELETE FROM MessagingSystemMessageBlocklist WHERE messageId = :messageId;`,
{
messageId,
}
);
}
);
} catch (e) {
lazy.ASRouterPreferences.console.error(
`ASRouterStorage: Failed writing to MessagingSystemMessageBlocklist`,
e
);
if (this.telemetry) {
this.telemetry.handleUndesiredEvent({
event: "SHARED_DB_WRITE_FAILED",
});
}
success = false;
}
}
lazy.ProfilesDatastoreService.notify();
return success;
}
}
export function getDefaultOptions(options) {
return { collapsed: !!options.collapsed };
}