зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1547877 - enable configuration of new Notification Store implementation r=mossop,lina
Differential Revision: https://phabricator.services.mozilla.com/D29305 --HG-- rename : dom/notification/NotificationDB.jsm => dom/notification/new/NotificationDB.jsm rename : dom/notification/NotificationDB.jsm => dom/notification/old/NotificationDB.jsm extra : moz-landing-system : lando
This commit is contained in:
Родитель
1b3f6d1faf
Коммит
926c25f9a4
|
@ -177,6 +177,12 @@ var whitelist = [
|
|||
{file: "resource://gre/localization/en-US/toolkit/about/aboutCompat.ftl"},
|
||||
];
|
||||
|
||||
if (!AppConstants.MOZ_NEW_NOTIFICATION_STORE) {
|
||||
// kvstore.jsm wraps the API in nsIKeyValue.idl in a more ergonomic API
|
||||
// It landed in bug 1490496, and we expect to start using it shortly.
|
||||
whitelist.push({file: "resource://gre/modules/kvstore.jsm"});
|
||||
}
|
||||
|
||||
whitelist = new Set(whitelist.filter(item =>
|
||||
("isFromDevTools" in item) == isDevtools &&
|
||||
(!item.skipUnofficial || !AppConstants.MOZILLA_OFFICIAL) &&
|
||||
|
|
|
@ -8,7 +8,6 @@ with Files("**"):
|
|||
BUG_COMPONENT = ("Toolkit", "Notifications and Alerts")
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'NotificationDB.jsm',
|
||||
'NotificationStorage.jsm',
|
||||
]
|
||||
|
||||
|
@ -42,3 +41,12 @@ MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
|
|||
|
||||
if CONFIG['CC_TYPE'] in ('clang', 'gcc'):
|
||||
CXXFLAGS += ['-Wno-error=shadow']
|
||||
|
||||
if CONFIG['MOZ_NEW_NOTIFICATION_STORE']:
|
||||
EXTRA_JS_MODULES += [
|
||||
'new/NotificationDB.jsm',
|
||||
]
|
||||
else:
|
||||
EXTRA_JS_MODULES += [
|
||||
'old/NotificationDB.jsm',
|
||||
]
|
||||
|
|
|
@ -0,0 +1,352 @@
|
|||
/* 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,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
var EXPORTED_SYMBOLS = [];
|
||||
|
||||
const DEBUG = false;
|
||||
function debug(s) { dump("-*- NotificationDB component: " + s + "\n"); }
|
||||
|
||||
const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "Services",
|
||||
"resource://gre/modules/Services.jsm");
|
||||
|
||||
const NOTIFICATION_STORE_DIR = OS.Constants.Path.profileDir;
|
||||
const NOTIFICATION_STORE_PATH =
|
||||
OS.Path.join(NOTIFICATION_STORE_DIR, "notificationstore.json");
|
||||
|
||||
const kMessages = [
|
||||
"Notification:Save",
|
||||
"Notification:Delete",
|
||||
"Notification:GetAll",
|
||||
];
|
||||
|
||||
var NotificationDB = {
|
||||
|
||||
// Ensure we won't call init() while xpcom-shutdown is performed
|
||||
_shutdownInProgress: false,
|
||||
|
||||
init() {
|
||||
if (this._shutdownInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.notifications = {};
|
||||
this.byTag = {};
|
||||
this.loaded = false;
|
||||
|
||||
this.tasks = []; // read/write operation queue
|
||||
this.runningTask = null;
|
||||
|
||||
Services.obs.addObserver(this, "xpcom-shutdown");
|
||||
this.registerListeners();
|
||||
},
|
||||
|
||||
registerListeners() {
|
||||
for (let message of kMessages) {
|
||||
Services.ppmm.addMessageListener(message, this);
|
||||
}
|
||||
},
|
||||
|
||||
unregisterListeners() {
|
||||
for (let message of kMessages) {
|
||||
Services.ppmm.removeMessageListener(message, this);
|
||||
}
|
||||
},
|
||||
|
||||
observe(aSubject, aTopic, aData) {
|
||||
if (DEBUG) debug("Topic: " + aTopic);
|
||||
if (aTopic == "xpcom-shutdown") {
|
||||
this._shutdownInProgress = true;
|
||||
Services.obs.removeObserver(this, "xpcom-shutdown");
|
||||
this.unregisterListeners();
|
||||
}
|
||||
},
|
||||
|
||||
filterNonAppNotifications(notifications) {
|
||||
for (let origin in notifications) {
|
||||
let persistentNotificationCount = 0;
|
||||
for (let id in notifications[origin]) {
|
||||
if (notifications[origin][id].serviceWorkerRegistrationScope) {
|
||||
persistentNotificationCount++;
|
||||
} else {
|
||||
delete notifications[origin][id];
|
||||
}
|
||||
}
|
||||
if (persistentNotificationCount == 0) {
|
||||
if (DEBUG) debug("Origin " + origin + " is not linked to an app manifest, deleting.");
|
||||
delete notifications[origin];
|
||||
}
|
||||
}
|
||||
|
||||
return notifications;
|
||||
},
|
||||
|
||||
// Attempt to read notification file, if it's not there we will create it.
|
||||
load() {
|
||||
var promise = OS.File.read(NOTIFICATION_STORE_PATH, { encoding: "utf-8"});
|
||||
return promise.then(
|
||||
data => {
|
||||
if (data.length > 0) {
|
||||
// Preprocessing phase intends to cleanly separate any migration-related
|
||||
// tasks.
|
||||
this.notifications = this.filterNonAppNotifications(JSON.parse(data));
|
||||
}
|
||||
|
||||
// populate the list of notifications by tag
|
||||
if (this.notifications) {
|
||||
for (var origin in this.notifications) {
|
||||
this.byTag[origin] = {};
|
||||
for (var id in this.notifications[origin]) {
|
||||
var curNotification = this.notifications[origin][id];
|
||||
if (curNotification.tag) {
|
||||
this.byTag[origin][curNotification.tag] = curNotification;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
},
|
||||
|
||||
// If read failed, we assume we have no notifications to load.
|
||||
reason => {
|
||||
this.loaded = true;
|
||||
return this.createStore();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// Creates the notification directory.
|
||||
createStore() {
|
||||
var promise = OS.File.makeDir(NOTIFICATION_STORE_DIR, {
|
||||
ignoreExisting: true,
|
||||
});
|
||||
return promise.then(
|
||||
this.createFile.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
// Creates the notification file once the directory is created.
|
||||
createFile() {
|
||||
return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, "");
|
||||
},
|
||||
|
||||
// Save current notifications to the file.
|
||||
save() {
|
||||
var data = JSON.stringify(this.notifications);
|
||||
return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, data, { encoding: "utf-8"});
|
||||
},
|
||||
|
||||
// Helper function: promise will be resolved once file exists and/or is loaded.
|
||||
ensureLoaded() {
|
||||
if (!this.loaded) {
|
||||
return this.load();
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
receiveMessage(message) {
|
||||
if (DEBUG) { debug("Received message:" + message.name); }
|
||||
|
||||
// sendAsyncMessage can fail if the child process exits during a
|
||||
// notification storage operation, so always wrap it in a try/catch.
|
||||
function returnMessage(name, data) {
|
||||
try {
|
||||
message.target.sendAsyncMessage(name, data);
|
||||
} catch (e) {
|
||||
if (DEBUG) { debug("Return message failed, " + name); }
|
||||
}
|
||||
}
|
||||
|
||||
switch (message.name) {
|
||||
case "Notification:GetAll":
|
||||
this.queueTask("getall", message.data).then(function(notifications) {
|
||||
returnMessage("Notification:GetAll:Return:OK", {
|
||||
requestID: message.data.requestID,
|
||||
origin: message.data.origin,
|
||||
notifications,
|
||||
});
|
||||
}).catch(function(error) {
|
||||
returnMessage("Notification:GetAll:Return:KO", {
|
||||
requestID: message.data.requestID,
|
||||
origin: message.data.origin,
|
||||
errorMsg: error,
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case "Notification:Save":
|
||||
this.queueTask("save", message.data).then(function() {
|
||||
returnMessage("Notification:Save:Return:OK", {
|
||||
requestID: message.data.requestID,
|
||||
});
|
||||
}).catch(function(error) {
|
||||
returnMessage("Notification:Save:Return:KO", {
|
||||
requestID: message.data.requestID,
|
||||
errorMsg: error,
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case "Notification:Delete":
|
||||
this.queueTask("delete", message.data).then(function() {
|
||||
returnMessage("Notification:Delete:Return:OK", {
|
||||
requestID: message.data.requestID,
|
||||
});
|
||||
}).catch(function(error) {
|
||||
returnMessage("Notification:Delete:Return:KO", {
|
||||
requestID: message.data.requestID,
|
||||
errorMsg: error,
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
if (DEBUG) { debug("Invalid message name" + message.name); }
|
||||
}
|
||||
},
|
||||
|
||||
// We need to make sure any read/write operations are atomic,
|
||||
// so use a queue to run each operation sequentially.
|
||||
queueTask(operation, data) {
|
||||
if (DEBUG) { debug("Queueing task: " + operation); }
|
||||
|
||||
var defer = {};
|
||||
|
||||
this.tasks.push({
|
||||
operation,
|
||||
data,
|
||||
defer,
|
||||
});
|
||||
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
defer.resolve = resolve;
|
||||
defer.reject = reject;
|
||||
});
|
||||
|
||||
// Only run immediately if we aren't currently running another task.
|
||||
if (!this.runningTask) {
|
||||
if (DEBUG) { debug("Task queue was not running, starting now..."); }
|
||||
this.runNextTask();
|
||||
}
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
runNextTask() {
|
||||
if (this.tasks.length === 0) {
|
||||
if (DEBUG) { debug("No more tasks to run, queue depleted"); }
|
||||
this.runningTask = null;
|
||||
return;
|
||||
}
|
||||
this.runningTask = this.tasks.shift();
|
||||
|
||||
// Always make sure we are loaded before performing any read/write tasks.
|
||||
this.ensureLoaded()
|
||||
.then(() => {
|
||||
var task = this.runningTask;
|
||||
|
||||
switch (task.operation) {
|
||||
case "getall":
|
||||
return this.taskGetAll(task.data);
|
||||
|
||||
case "save":
|
||||
return this.taskSave(task.data);
|
||||
|
||||
case "delete":
|
||||
return this.taskDelete(task.data);
|
||||
|
||||
default:
|
||||
return Promise.reject(
|
||||
new Error(`Found a task with unknown operation ${task.operation}`));
|
||||
}
|
||||
})
|
||||
.then(payload => {
|
||||
if (DEBUG) {
|
||||
debug("Finishing task: " + this.runningTask.operation);
|
||||
}
|
||||
this.runningTask.defer.resolve(payload);
|
||||
})
|
||||
.catch(err => {
|
||||
if (DEBUG) {
|
||||
debug("Error while running " + this.runningTask.operation + ": " + err);
|
||||
}
|
||||
this.runningTask.defer.reject(err);
|
||||
})
|
||||
.then(() => {
|
||||
this.runNextTask();
|
||||
});
|
||||
},
|
||||
|
||||
taskGetAll(data) {
|
||||
if (DEBUG) { debug("Task, getting all"); }
|
||||
var origin = data.origin;
|
||||
var notifications = [];
|
||||
// Grab only the notifications for specified origin.
|
||||
if (this.notifications[origin]) {
|
||||
if (data.tag) {
|
||||
let n;
|
||||
if ((n = this.byTag[origin][data.tag])) {
|
||||
notifications.push(n);
|
||||
}
|
||||
} else {
|
||||
for (var i in this.notifications[origin]) {
|
||||
notifications.push(this.notifications[origin][i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(notifications);
|
||||
},
|
||||
|
||||
taskSave(data) {
|
||||
if (DEBUG) { debug("Task, saving"); }
|
||||
var origin = data.origin;
|
||||
var notification = data.notification;
|
||||
if (!this.notifications[origin]) {
|
||||
this.notifications[origin] = {};
|
||||
this.byTag[origin] = {};
|
||||
}
|
||||
|
||||
// We might have existing notification with this tag,
|
||||
// if so we need to remove it before saving the new one.
|
||||
if (notification.tag) {
|
||||
var oldNotification = this.byTag[origin][notification.tag];
|
||||
if (oldNotification) {
|
||||
delete this.notifications[origin][oldNotification.id];
|
||||
}
|
||||
this.byTag[origin][notification.tag] = notification;
|
||||
}
|
||||
|
||||
this.notifications[origin][notification.id] = notification;
|
||||
return this.save();
|
||||
},
|
||||
|
||||
taskDelete(data) {
|
||||
if (DEBUG) { debug("Task, deleting"); }
|
||||
var origin = data.origin;
|
||||
var id = data.id;
|
||||
if (!this.notifications[origin]) {
|
||||
if (DEBUG) { debug("No notifications found for origin: " + origin); }
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Make sure we can find the notification to delete.
|
||||
var oldNotification = this.notifications[origin][id];
|
||||
if (!oldNotification) {
|
||||
if (DEBUG) { debug("No notification found with id: " + id); }
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (oldNotification.tag) {
|
||||
delete this.byTag[origin][oldNotification.tag];
|
||||
}
|
||||
delete this.notifications[origin][id];
|
||||
return this.save();
|
||||
},
|
||||
};
|
||||
|
||||
NotificationDB.init();
|
|
@ -1,5 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
|
||||
const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
|
||||
|
||||
const fooNotification =
|
||||
|
@ -15,7 +16,14 @@ const OLD_STORE_PATH =
|
|||
|
||||
let nextRequestID = 0;
|
||||
|
||||
async function createOldDatastore() {
|
||||
// Create the old datastore and populate it with data before we initialize
|
||||
// the notification database so it has data to migrate. This is a setup step,
|
||||
// not a test, but it seems like we need to do it in a test function
|
||||
// rather than in run_test() because the test runner doesn't handle async steps
|
||||
// in run_test().
|
||||
add_task({
|
||||
skip_if: () => !AppConstants.MOZ_NEW_NOTIFICATION_STORE,
|
||||
}, async function test_create_old_datastore() {
|
||||
const notifications = {
|
||||
[fooNotification.origin]: {
|
||||
[fooNotification.id]: fooNotification,
|
||||
|
@ -26,19 +34,13 @@ async function createOldDatastore() {
|
|||
};
|
||||
|
||||
await OS.File.writeAtomic(OLD_STORE_PATH, JSON.stringify(notifications));
|
||||
}
|
||||
|
||||
// Create the old datastore and populate it with data before we initialize
|
||||
// the notification database so it has data to migrate. This is a setup step,
|
||||
// not a test, but it seems like we need to do it in a test function
|
||||
// rather than in run_test() because the test runner doesn't handle async steps
|
||||
// in run_test().
|
||||
add_task(async function test_create_old_datastore() {
|
||||
await createOldDatastore();
|
||||
startNotificationDB();
|
||||
});
|
||||
|
||||
add_test(function test_get_system_notification() {
|
||||
add_test({
|
||||
skip_if: () => !AppConstants.MOZ_NEW_NOTIFICATION_STORE,
|
||||
}, function test_get_system_notification() {
|
||||
const requestID = nextRequestID++;
|
||||
const msgHandler = function(message) {
|
||||
Assert.equal(requestID, message.data.requestID);
|
||||
|
@ -51,7 +53,9 @@ add_test(function test_get_system_notification() {
|
|||
});
|
||||
});
|
||||
|
||||
add_test(function test_get_foo_notification() {
|
||||
add_test({
|
||||
skip_if: () => !AppConstants.MOZ_NEW_NOTIFICATION_STORE,
|
||||
}, function test_get_foo_notification() {
|
||||
const requestID = nextRequestID++;
|
||||
const msgHandler = function(message) {
|
||||
Assert.equal(requestID, message.data.requestID);
|
||||
|
@ -66,7 +70,9 @@ add_test(function test_get_foo_notification() {
|
|||
});
|
||||
});
|
||||
|
||||
add_test(function test_get_bar_notification() {
|
||||
add_test({
|
||||
skip_if: () => !AppConstants.MOZ_NEW_NOTIFICATION_STORE,
|
||||
}, function test_get_bar_notification() {
|
||||
const requestID = nextRequestID++;
|
||||
const msgHandler = function(message) {
|
||||
Assert.equal(requestID, message.data.requestID);
|
||||
|
@ -81,7 +87,9 @@ add_test(function test_get_bar_notification() {
|
|||
});
|
||||
});
|
||||
|
||||
add_task(async function test_old_datastore_deleted() {
|
||||
add_task({
|
||||
skip_if: () => !AppConstants.MOZ_NEW_NOTIFICATION_STORE,
|
||||
}, async function test_old_datastore_deleted() {
|
||||
Assert.ok(!await OS.File.exists(OLD_STORE_PATH),
|
||||
"old datastore no longer exists");
|
||||
});
|
||||
|
|
|
@ -357,4 +357,11 @@ this.AppConstants = Object.freeze({
|
|||
#else
|
||||
false,
|
||||
#endif
|
||||
|
||||
MOZ_NEW_NOTIFICATION_STORE:
|
||||
#ifdef MOZ_NEW_NOTIFICATION_STORE
|
||||
true,
|
||||
#else
|
||||
false,
|
||||
#endif
|
||||
});
|
||||
|
|
|
@ -1738,3 +1738,15 @@ def new_xulstore(milestone):
|
|||
|
||||
set_config('MOZ_NEW_XULSTORE', True, when=new_xulstore)
|
||||
set_define('MOZ_NEW_XULSTORE', True, when=new_xulstore)
|
||||
|
||||
|
||||
# new Notification Store implementation
|
||||
# ==============================================================
|
||||
|
||||
@depends(milestone)
|
||||
def new_notification_store(milestone):
|
||||
if milestone.is_nightly:
|
||||
return True
|
||||
|
||||
set_config('MOZ_NEW_NOTIFICATION_STORE', True, when=new_notification_store)
|
||||
set_define('MOZ_NEW_NOTIFICATION_STORE', True, when=new_notification_store)
|
||||
|
|
Загрузка…
Ссылка в новой задаче