Loading toolkit/components/extensions/child/ext-storage.js +36 −22 Original line number Diff line number Diff line Loading @@ -151,6 +151,35 @@ this.storage = class extends ExtensionAPI { context ); // onChangedName is "storage.onChanged", "storage.sync.onChanged", etc. function makeOnChangedEventTarget(onChangedName) { return new EventManager({ context, name: onChangedName, register: fire => { let onChanged = (data, area) => { let changes = new context.cloneScope.Object(); for (let [key, value] of Object.entries(data)) { changes[key] = deserialize(value); } if (area) { // storage.onChanged includes the area. fire.raw(changes, area); } else { // StorageArea.onChanged doesn't include the area. fire.raw(changes); } }; let parent = context.childManager.getParentEvent(onChangedName); parent.addListener(onChanged); return () => { parent.removeListener(onChanged); }; }, }).api(); } function sanitize(items) { // The schema validator already takes care of arrays (which are only allowed // to contain strings). Strings and null are safe values. Loading Loading @@ -222,7 +251,9 @@ this.storage = class extends ExtensionAPI { let promiseStorageLocalBackend; // Generate the backend-agnostic local API wrapped methods. const local = {}; const local = { onChanged: makeOnChangedEventTarget("storage.local.onChanged"), }; for (let method of ["get", "set", "remove", "clear"]) { local[method] = async function(...args) { try { Loading Loading @@ -293,6 +324,7 @@ this.storage = class extends ExtensionAPI { [items] ); }, onChanged: makeOnChangedEventTarget("storage.sync.onChanged"), }, managed: { Loading @@ -310,29 +342,11 @@ this.storage = class extends ExtensionAPI { clear() { return Promise.reject({ message: "storage.managed is read-only" }); }, }, onChanged: new EventManager({ context, name: "storage.onChanged", register: fire => { let onChanged = (data, area) => { let changes = new context.cloneScope.Object(); for (let [key, value] of Object.entries(data)) { changes[key] = deserialize(value); } fire.raw(changes, area); }; let parent = context.childManager.getParentEvent( "storage.onChanged" ); parent.addListener(onChanged); return () => { parent.removeListener(onChanged); }; onChanged: makeOnChangedEventTarget("storage.managed.onChanged"), }, }).api(), onChanged: makeOnChangedEventTarget("storage.onChanged"), }, }; } Loading toolkit/components/extensions/parent/ext-storage.js +39 −0 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { }); var { ExtensionError } = ExtensionUtils; var { ignoreEvent } = ExtensionCommon; XPCOMUtils.defineLazyGetter(this, "extensionStorageSync", () => { // TODO bug 1637465: Remove Kinto-based implementation. Loading Loading @@ -85,6 +86,30 @@ this.storage = class extends ExtensionAPIPersistent { }, }; }, "local.onChanged"({ fire }) { let unregister = this.registerLocalChangedListener(changes => { // |changes| is already serialized. Send the raw value, so that it can // be deserialized by the onChanged handler in child/ext-storage.js. fire.raw(changes); }); return { unregister, convert(_fire) { fire = _fire; }, }; }, "sync.onChanged"({ fire }) { let unregister = this.registerSyncChangedListener(changes => { fire.async(changes); }); return { unregister, convert(_fire) { fire = _fire; }, }; }, }; registerLocalChangedListener(onStorageLocalChanged) { Loading Loading @@ -213,6 +238,12 @@ this.storage = class extends ExtensionAPIPersistent { return ExtensionStorageIDB.selectBackend(context); }, }, onChanged: new EventManager({ context, module: "storage", event: "local.onChanged", extensionApi: this, }).api(), }, sync: { Loading @@ -236,6 +267,12 @@ this.storage = class extends ExtensionAPIPersistent { enforceNoTemporaryAddon(extension.id); return extensionStorageSync.getBytesInUse(extension, keys, context); }, onChanged: new EventManager({ context, module: "storage", event: "sync.onChanged", extensionApi: this, }).api(), }, managed: { Loading @@ -256,6 +293,8 @@ this.storage = class extends ExtensionAPIPersistent { } return ExtensionStorage._filterProperties(data, keys); }, // managed storage is currently initialized once. onChanged: ignoreEvent(context, "storage.managed.onChanged"), }, onChanged: new EventManager({ Loading toolkit/components/extensions/schemas/storage.json +30 −0 Original line number Diff line number Diff line Loading @@ -154,6 +154,21 @@ } ] } ], "events": [ { "name": "onChanged", "type": "function", "description": "Fired when one or more items change.", "parameters": [ { "name": "changes", "type": "object", "additionalProperties": { "$ref": "StorageChange" }, "description": "Object mapping each key that changed to its corresponding $(ref:storage.StorageChange) for that item." } ] } ] }, { Loading Loading @@ -283,6 +298,21 @@ } ] } ], "events": [ { "name": "onChanged", "type": "function", "description": "Fired when one or more items change.", "parameters": [ { "name": "changes", "type": "object", "additionalProperties": { "$ref": "StorageChange" }, "description": "Object mapping each key that changed to its corresponding $(ref:storage.StorageChange) for that item." } ] } ] } ], Loading toolkit/components/extensions/test/xpcshell/head_storage.js +33 −5 Original line number Diff line number Diff line Loading @@ -1242,8 +1242,8 @@ async function test_contentscript_storage(storageType) { } async function test_storage_change_event_page(areaName) { async function testFn() { function background(areaName) { async function testOnChanged(targetIsStorageArea) { function backgroundTestStorageTopNamespace(areaName) { browser.storage.onChanged.addListener((changes, area) => { browser.test.assertEq(area, areaName, "Expected areaName"); browser.test.assertEq( Loading @@ -1254,6 +1254,26 @@ async function test_storage_change_event_page(areaName) { browser.test.sendMessage("onChanged_was_fired"); }); } function backgroundTestStorageAreaNamespace(areaName) { browser.storage[areaName].onChanged.addListener((changes, ...args) => { browser.test.assertEq(args.length, 0, "no more args after changes"); browser.test.assertEq( JSON.stringify(changes), `{"storageKey":{"newValue":"newStorageValue"}}`, `Expected changes via ${areaName}.onChanged event` ); browser.test.sendMessage("onChanged_was_fired"); }); } let background, onChangedName; if (targetIsStorageArea) { // Test storage.local.onChanged / storage.sync.onChanged. background = backgroundTestStorageAreaNamespace; onChangedName = `${areaName}.onChanged`; } else { background = backgroundTestStorageTopNamespace; onChangedName = "onChanged"; } let extension = ExtensionTestUtils.loadExtension({ manifest: { permissions: ["storage"], Loading @@ -1275,12 +1295,12 @@ async function test_storage_change_event_page(areaName) { }, }); await extension.startup(); assertPersistentListeners(extension, "storage", "onChanged", { assertPersistentListeners(extension, "storage", onChangedName, { primed: false, }); await extension.terminateBackground(); assertPersistentListeners(extension, "storage", "onChanged", { assertPersistentListeners(extension, "storage", onChangedName, { primed: true, }); Loading @@ -1292,11 +1312,19 @@ async function test_storage_change_event_page(areaName) { await contentPage.close(); await extension.awaitMessage("onChanged_was_fired"); assertPersistentListeners(extension, "storage", "onChanged", { assertPersistentListeners(extension, "storage", onChangedName, { primed: false, }); await extension.unload(); } async function testFn() { // Test browser.storage.onChanged.addListener await testOnChanged(/* targetIsStorageArea */ false); // Test browser.storage.local.onChanged.addListener // and browser.storage.sync.onChanged.addListener, depending on areaName. await testOnChanged(/* targetIsStorageArea */ true); } return runWithPrefs([["extensions.eventPages.enabled", true]], testFn); } toolkit/components/extensions/test/xpcshell/test_ext_storage_managed.js +43 −0 Original line number Diff line number Diff line Loading @@ -168,3 +168,46 @@ add_task(async function test_manifest_not_found() { await extension.awaitFinish(); await extension.unload(); }); add_task(async function test_manifest_not_found() { let extension = ExtensionTestUtils.loadExtension({ manifest: { permissions: ["storage"], }, async background() { const dummyListener = () => {}; browser.storage.managed.onChanged.addListener(dummyListener); browser.test.assertTrue( browser.storage.managed.onChanged.hasListener(dummyListener), "addListener works according to hasListener" ); browser.storage.managed.onChanged.removeListener(dummyListener); // We should get a warning for each registration. browser.storage.managed.onChanged.addListener(() => {}); browser.storage.managed.onChanged.addListener(() => {}); browser.storage.managed.onChanged.addListener(() => {}); // Invoke the storage.managed API to make sure that we have made a // round trip to the parent process and back. This is because event // registration is async but we cannot await (bug 1300234). await browser.test.assertRejects( browser.storage.managed.get({ a: 1 }), /Managed storage manifest not found/, "browser.storage.managed.get() rejects when without manifest" ); browser.test.notifyPass(); }, }); let { messages } = await promiseConsoleOutput(async () => { await extension.startup(); await extension.awaitFinish(); await extension.unload(); }); const UNSUP_EVENT_WARNING = `attempting to use listener "storage.managed.onChanged", which is unimplemented`; messages = messages.filter(msg => msg.message.includes(UNSUP_EVENT_WARNING)); Assert.equal(messages.length, 4, "Expected msg for each addListener call"); }); Loading
toolkit/components/extensions/child/ext-storage.js +36 −22 Original line number Diff line number Diff line Loading @@ -151,6 +151,35 @@ this.storage = class extends ExtensionAPI { context ); // onChangedName is "storage.onChanged", "storage.sync.onChanged", etc. function makeOnChangedEventTarget(onChangedName) { return new EventManager({ context, name: onChangedName, register: fire => { let onChanged = (data, area) => { let changes = new context.cloneScope.Object(); for (let [key, value] of Object.entries(data)) { changes[key] = deserialize(value); } if (area) { // storage.onChanged includes the area. fire.raw(changes, area); } else { // StorageArea.onChanged doesn't include the area. fire.raw(changes); } }; let parent = context.childManager.getParentEvent(onChangedName); parent.addListener(onChanged); return () => { parent.removeListener(onChanged); }; }, }).api(); } function sanitize(items) { // The schema validator already takes care of arrays (which are only allowed // to contain strings). Strings and null are safe values. Loading Loading @@ -222,7 +251,9 @@ this.storage = class extends ExtensionAPI { let promiseStorageLocalBackend; // Generate the backend-agnostic local API wrapped methods. const local = {}; const local = { onChanged: makeOnChangedEventTarget("storage.local.onChanged"), }; for (let method of ["get", "set", "remove", "clear"]) { local[method] = async function(...args) { try { Loading Loading @@ -293,6 +324,7 @@ this.storage = class extends ExtensionAPI { [items] ); }, onChanged: makeOnChangedEventTarget("storage.sync.onChanged"), }, managed: { Loading @@ -310,29 +342,11 @@ this.storage = class extends ExtensionAPI { clear() { return Promise.reject({ message: "storage.managed is read-only" }); }, }, onChanged: new EventManager({ context, name: "storage.onChanged", register: fire => { let onChanged = (data, area) => { let changes = new context.cloneScope.Object(); for (let [key, value] of Object.entries(data)) { changes[key] = deserialize(value); } fire.raw(changes, area); }; let parent = context.childManager.getParentEvent( "storage.onChanged" ); parent.addListener(onChanged); return () => { parent.removeListener(onChanged); }; onChanged: makeOnChangedEventTarget("storage.managed.onChanged"), }, }).api(), onChanged: makeOnChangedEventTarget("storage.onChanged"), }, }; } Loading
toolkit/components/extensions/parent/ext-storage.js +39 −0 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { }); var { ExtensionError } = ExtensionUtils; var { ignoreEvent } = ExtensionCommon; XPCOMUtils.defineLazyGetter(this, "extensionStorageSync", () => { // TODO bug 1637465: Remove Kinto-based implementation. Loading Loading @@ -85,6 +86,30 @@ this.storage = class extends ExtensionAPIPersistent { }, }; }, "local.onChanged"({ fire }) { let unregister = this.registerLocalChangedListener(changes => { // |changes| is already serialized. Send the raw value, so that it can // be deserialized by the onChanged handler in child/ext-storage.js. fire.raw(changes); }); return { unregister, convert(_fire) { fire = _fire; }, }; }, "sync.onChanged"({ fire }) { let unregister = this.registerSyncChangedListener(changes => { fire.async(changes); }); return { unregister, convert(_fire) { fire = _fire; }, }; }, }; registerLocalChangedListener(onStorageLocalChanged) { Loading Loading @@ -213,6 +238,12 @@ this.storage = class extends ExtensionAPIPersistent { return ExtensionStorageIDB.selectBackend(context); }, }, onChanged: new EventManager({ context, module: "storage", event: "local.onChanged", extensionApi: this, }).api(), }, sync: { Loading @@ -236,6 +267,12 @@ this.storage = class extends ExtensionAPIPersistent { enforceNoTemporaryAddon(extension.id); return extensionStorageSync.getBytesInUse(extension, keys, context); }, onChanged: new EventManager({ context, module: "storage", event: "sync.onChanged", extensionApi: this, }).api(), }, managed: { Loading @@ -256,6 +293,8 @@ this.storage = class extends ExtensionAPIPersistent { } return ExtensionStorage._filterProperties(data, keys); }, // managed storage is currently initialized once. onChanged: ignoreEvent(context, "storage.managed.onChanged"), }, onChanged: new EventManager({ Loading
toolkit/components/extensions/schemas/storage.json +30 −0 Original line number Diff line number Diff line Loading @@ -154,6 +154,21 @@ } ] } ], "events": [ { "name": "onChanged", "type": "function", "description": "Fired when one or more items change.", "parameters": [ { "name": "changes", "type": "object", "additionalProperties": { "$ref": "StorageChange" }, "description": "Object mapping each key that changed to its corresponding $(ref:storage.StorageChange) for that item." } ] } ] }, { Loading Loading @@ -283,6 +298,21 @@ } ] } ], "events": [ { "name": "onChanged", "type": "function", "description": "Fired when one or more items change.", "parameters": [ { "name": "changes", "type": "object", "additionalProperties": { "$ref": "StorageChange" }, "description": "Object mapping each key that changed to its corresponding $(ref:storage.StorageChange) for that item." } ] } ] } ], Loading
toolkit/components/extensions/test/xpcshell/head_storage.js +33 −5 Original line number Diff line number Diff line Loading @@ -1242,8 +1242,8 @@ async function test_contentscript_storage(storageType) { } async function test_storage_change_event_page(areaName) { async function testFn() { function background(areaName) { async function testOnChanged(targetIsStorageArea) { function backgroundTestStorageTopNamespace(areaName) { browser.storage.onChanged.addListener((changes, area) => { browser.test.assertEq(area, areaName, "Expected areaName"); browser.test.assertEq( Loading @@ -1254,6 +1254,26 @@ async function test_storage_change_event_page(areaName) { browser.test.sendMessage("onChanged_was_fired"); }); } function backgroundTestStorageAreaNamespace(areaName) { browser.storage[areaName].onChanged.addListener((changes, ...args) => { browser.test.assertEq(args.length, 0, "no more args after changes"); browser.test.assertEq( JSON.stringify(changes), `{"storageKey":{"newValue":"newStorageValue"}}`, `Expected changes via ${areaName}.onChanged event` ); browser.test.sendMessage("onChanged_was_fired"); }); } let background, onChangedName; if (targetIsStorageArea) { // Test storage.local.onChanged / storage.sync.onChanged. background = backgroundTestStorageAreaNamespace; onChangedName = `${areaName}.onChanged`; } else { background = backgroundTestStorageTopNamespace; onChangedName = "onChanged"; } let extension = ExtensionTestUtils.loadExtension({ manifest: { permissions: ["storage"], Loading @@ -1275,12 +1295,12 @@ async function test_storage_change_event_page(areaName) { }, }); await extension.startup(); assertPersistentListeners(extension, "storage", "onChanged", { assertPersistentListeners(extension, "storage", onChangedName, { primed: false, }); await extension.terminateBackground(); assertPersistentListeners(extension, "storage", "onChanged", { assertPersistentListeners(extension, "storage", onChangedName, { primed: true, }); Loading @@ -1292,11 +1312,19 @@ async function test_storage_change_event_page(areaName) { await contentPage.close(); await extension.awaitMessage("onChanged_was_fired"); assertPersistentListeners(extension, "storage", "onChanged", { assertPersistentListeners(extension, "storage", onChangedName, { primed: false, }); await extension.unload(); } async function testFn() { // Test browser.storage.onChanged.addListener await testOnChanged(/* targetIsStorageArea */ false); // Test browser.storage.local.onChanged.addListener // and browser.storage.sync.onChanged.addListener, depending on areaName. await testOnChanged(/* targetIsStorageArea */ true); } return runWithPrefs([["extensions.eventPages.enabled", true]], testFn); }
toolkit/components/extensions/test/xpcshell/test_ext_storage_managed.js +43 −0 Original line number Diff line number Diff line Loading @@ -168,3 +168,46 @@ add_task(async function test_manifest_not_found() { await extension.awaitFinish(); await extension.unload(); }); add_task(async function test_manifest_not_found() { let extension = ExtensionTestUtils.loadExtension({ manifest: { permissions: ["storage"], }, async background() { const dummyListener = () => {}; browser.storage.managed.onChanged.addListener(dummyListener); browser.test.assertTrue( browser.storage.managed.onChanged.hasListener(dummyListener), "addListener works according to hasListener" ); browser.storage.managed.onChanged.removeListener(dummyListener); // We should get a warning for each registration. browser.storage.managed.onChanged.addListener(() => {}); browser.storage.managed.onChanged.addListener(() => {}); browser.storage.managed.onChanged.addListener(() => {}); // Invoke the storage.managed API to make sure that we have made a // round trip to the parent process and back. This is because event // registration is async but we cannot await (bug 1300234). await browser.test.assertRejects( browser.storage.managed.get({ a: 1 }), /Managed storage manifest not found/, "browser.storage.managed.get() rejects when without manifest" ); browser.test.notifyPass(); }, }); let { messages } = await promiseConsoleOutput(async () => { await extension.startup(); await extension.awaitFinish(); await extension.unload(); }); const UNSUP_EVENT_WARNING = `attempting to use listener "storage.managed.onChanged", which is unimplemented`; messages = messages.filter(msg => msg.message.includes(UNSUP_EVENT_WARNING)); Assert.equal(messages.length, 4, "Expected msg for each addListener call"); });