diff --git a/browser/base/content/tabbrowser.js b/browser/base/content/tabbrowser.js
index 75513e7d2263..96928a9cf879 100644
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -63,6 +63,7 @@
XPCOMUtils.defineLazyModuleGetters(this, {
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
+ PictureInPicture: "resource://gre/modules/PictureInPicture.jsm",
});
XPCOMUtils.defineLazyServiceGetters(this, {
MacSharingService: [
@@ -5290,7 +5291,8 @@
(aBrowser == this.selectedBrowser &&
window.windowState != window.STATE_MINIMIZED &&
!window.isFullyOccluded) ||
- this._printPreviewBrowsers.has(aBrowser)
+ this._printPreviewBrowsers.has(aBrowser) ||
+ this.PictureInPicture.isOriginatingBrowser(aBrowser)
);
},
diff --git a/browser/base/content/upgradeDialog.js b/browser/base/content/upgradeDialog.js
index 3184d886531b..60139630d799 100644
--- a/browser/base/content/upgradeDialog.js
+++ b/browser/base/content/upgradeDialog.js
@@ -212,7 +212,7 @@ function onLoad(ready) {
1;
const enableVariant = () =>
enableTheme(
- THEME_IDS[variations.getAttribute("next")][getVariantIndex()]
+ THEME_IDS[variations.getAttribute("next") ?? 0][getVariantIndex()]
);
// Prepare random theme selection that's not (first) default.
diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm
index fde57768680e..9fbc5871566c 100644
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -993,35 +993,6 @@ BrowserGlue.prototype = {
Services.prefs.savePrefFile(null);
},
- _setSyncAutoconnectDelay: function BG__setSyncAutoconnectDelay() {
- // Assume that a non-zero value for services.sync.autoconnectDelay should override
- if (Services.prefs.prefHasUserValue("services.sync.autoconnectDelay")) {
- let prefDelay = Services.prefs.getIntPref(
- "services.sync.autoconnectDelay"
- );
-
- if (prefDelay > 0) {
- return;
- }
- }
-
- // delays are in seconds
- const MAX_DELAY = 300;
- let delay = 3;
- for (let win of Services.wm.getEnumerator("navigator:browser")) {
- // browser windows without a gBrowser almost certainly means we are
- // shutting down, so instead of just ignoring that window we abort.
- if (win.closed || !win.gBrowser) {
- return;
- }
- delay += win.gBrowser.tabs.length;
- }
- delay = delay <= MAX_DELAY ? delay : MAX_DELAY;
-
- const { Weave } = ChromeUtils.import("resource://services-sync/main.js");
- Weave.Service.scheduler.delayedAutoConnect(delay);
- },
-
// nsIObserver implementation
observe: async function BG_observe(subject, topic, data) {
switch (topic) {
@@ -1064,9 +1035,6 @@ BrowserGlue.prototype = {
this._setPrefToSaveSession();
}
break;
- case "weave:service:ready":
- this._setSyncAutoconnectDelay();
- break;
case "fxaccounts:onverified":
this._onThisDeviceConnected();
break;
@@ -1232,7 +1200,6 @@ BrowserGlue.prototype = {
"browser:purge-session-history",
"quit-application-requested",
"quit-application-granted",
- "weave:service:ready",
"fxaccounts:onverified",
"fxaccounts:device_connected",
"fxaccounts:verify_login",
@@ -2765,6 +2732,15 @@ BrowserGlue.prototype = {
this._collectTelemetryPiPEnabled();
},
},
+ // Schedule a sync (if enabled) after we've loaded
+ {
+ task: async () => {
+ if (WeaveService.enabled) {
+ await WeaveService.whenLoaded();
+ WeaveService.Weave.Service.scheduler.autoConnect();
+ }
+ },
+ },
{
condition: AppConstants.platform == "win",
diff --git a/browser/components/pocket/content/panels/css/main.compiled.css b/browser/components/pocket/content/panels/css/main.compiled.css
index 97558b9b6ee4..c64732abfa7f 100644
--- a/browser/components/pocket/content/panels/css/main.compiled.css
+++ b/browser/components/pocket/content/panels/css/main.compiled.css
@@ -1987,6 +1987,45 @@ button {
margin: 20px 0;
}
+.stp_tag_picker .stp_tag_picker_tags {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 8px;
+ border: 1px solid #8F8F9D;
+ border-radius: 4px;
+ font-style: normal;
+ font-weight: normal;
+ font-size: 1em;
+ line-height: 1.2em;
+ color: #15141A;
+ margin-bottom: 10px;
+}
+.stp_tag_picker .stp_tag_picker_tag {
+ background: #F0F0F4;
+ border-radius: 4px;
+ color: #15141A;
+ display: inline-block;
+ font-size: 0.7em;
+ font-style: normal;
+ line-height: 1em;
+ font-weight: 600;
+ margin-bottom: 8px;
+ margin-inline-end: 8px;
+ padding: 4px 8px;
+ transition: background-color 200ms ease-in-out;
+}
+.stp_tag_picker .stp_tag_picker_tag_remove {
+ padding: 5px;
+ color: #5B5B66;
+ font-weight: 400;
+}
+.stp_tag_picker .stp_tag_picker_tag_duplicate {
+ background-color: #bbb;
+}
+.stp_tag_picker .stp_tag_picker_input {
+ flex-grow: 1;
+}
+
.stp_popular_topics {
padding: 0;
}
diff --git a/browser/components/pocket/content/panels/css/main.scss b/browser/components/pocket/content/panels/css/main.scss
index ebb8425162c0..c4a8896985dd 100644
--- a/browser/components/pocket/content/panels/css/main.scss
+++ b/browser/components/pocket/content/panels/css/main.scss
@@ -8,6 +8,7 @@
// Components
+@import "../js/components/TagPicker/TagPicker";
@import "../js/components/PopularTopics/PopularTopics";
@import "../js/components/ArticleList/ArticleList";
@import "../js/components/Header/Header";
diff --git a/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.jsx b/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.jsx
new file mode 100644
index 000000000000..d0a0330cd560
--- /dev/null
+++ b/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.jsx
@@ -0,0 +1,77 @@
+/* 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/. */
+
+import React, { useState } from "react";
+
+function TagPicker(props) {
+ const [tags, setTags] = useState(props.tags);
+ const [duplicateTag, setDuplicateTag] = useState(null);
+
+ let handleKeyDown = e => {
+ // Enter tag on comma or enter keypress
+ if (e.keyCode === 188 || e.keyCode === 13) {
+ let tag = e.target.value.trim();
+
+ e.preventDefault();
+ e.target.value = ``; // Clear out input
+
+ addTag(tag);
+ }
+ };
+
+ let addTag = tag => {
+ let newDuplicateTag = tags.find(item => item === tag);
+
+ if (!tag.length) {
+ return;
+ }
+
+ if (!newDuplicateTag) {
+ setTags([...tags, tag]);
+ } else {
+ setDuplicateTag(newDuplicateTag);
+
+ setTimeout(() => {
+ setDuplicateTag(null);
+ }, 1000);
+ }
+ };
+
+ let removeTag = index => {
+ let updatedTags = tags.slice(0); // Shallow copied array
+ updatedTags.splice(index, 1);
+ setTags(updatedTags);
+ };
+
+ return (
+
+
Add Tags:
+
+ {tags.map((tag, i) => (
+
+ {tag}
+
+
+ ))}
+
handleKeyDown(e)}
+ maxlength="25"
+ >
+
+
+ );
+}
+
+export default TagPicker;
diff --git a/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.scss b/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.scss
new file mode 100644
index 000000000000..af57d8511a87
--- /dev/null
+++ b/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.scss
@@ -0,0 +1,44 @@
+.stp_tag_picker {
+ .stp_tag_picker_tags {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 8px;
+ border: 1px solid #8F8F9D;
+ border-radius: 4px;
+ font-style: normal;
+ font-weight: normal;
+ font-size: 1em;
+ line-height: 1.2em;
+ color: #15141A;
+ margin-bottom: 10px;
+ }
+
+ .stp_tag_picker_tag {
+ background: #F0F0F4;
+ border-radius: 4px;
+ color: #15141A;
+ display: inline-block;
+ font-size: 0.7em;
+ font-style: normal;
+ line-height: 1em;
+ font-weight: 600;
+ margin-bottom: 8px;
+ margin-inline-end: 8px;
+ padding: 4px 8px;
+ transition: background-color 200ms ease-in-out;
+ }
+
+ .stp_tag_picker_tag_remove {
+ padding: 5px;
+ color: #5B5B66;
+ font-weight: 400;
+ }
+
+ .stp_tag_picker_tag_duplicate {
+ background-color: #bbb;
+ }
+
+ .stp_tag_picker_input {
+ flex-grow: 1;
+ }
+}
\ No newline at end of file
diff --git a/browser/components/pocket/content/panels/js/main.bundle.js b/browser/components/pocket/content/panels/js/main.bundle.js
index 9f75517be3c5..2c63ae011df7 100644
--- a/browser/components/pocket/content/panels/js/main.bundle.js
+++ b/browser/components/pocket/content/panels/js/main.bundle.js
@@ -2,7 +2,7 @@
/******/ "use strict";
/******/ var __webpack_modules__ = ({
-/***/ 861:
+/***/ 503:
/***/ ((__unused_webpack_module, __unused_webpack___webpack_exports__, __webpack_require__) => {
@@ -1192,6 +1192,69 @@ SavedOverlay.prototype = {
};
/* harmony default export */ const saved_overlay = (SavedOverlay);
+;// CONCATENATED MODULE: ./content/panels/js/components/TagPicker/TagPicker.jsx
+/* 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/. */
+
+
+function TagPicker(props) {
+ const [tags, setTags] = (0,react.useState)(props.tags);
+ const [duplicateTag, setDuplicateTag] = (0,react.useState)(null);
+
+ let handleKeyDown = e => {
+ // Enter tag on comma or enter keypress
+ if (e.keyCode === 188 || e.keyCode === 13) {
+ let tag = e.target.value.trim();
+ e.preventDefault();
+ e.target.value = ``; // Clear out input
+
+ addTag(tag);
+ }
+ };
+
+ let addTag = tag => {
+ let newDuplicateTag = tags.find(item => item === tag);
+
+ if (!tag.length) {
+ return;
+ }
+
+ if (!newDuplicateTag) {
+ setTags([...tags, tag]);
+ } else {
+ setDuplicateTag(newDuplicateTag);
+ setTimeout(() => {
+ setDuplicateTag(null);
+ }, 1000);
+ }
+ };
+
+ let removeTag = index => {
+ let updatedTags = tags.slice(0); // Shallow copied array
+
+ updatedTags.splice(index, 1);
+ setTags(updatedTags);
+ };
+
+ return /*#__PURE__*/react.createElement("div", {
+ className: "stp_tag_picker"
+ }, /*#__PURE__*/react.createElement("p", null, "Add Tags:"), /*#__PURE__*/react.createElement("div", {
+ className: "stp_tag_picker_tags"
+ }, tags.map((tag, i) => /*#__PURE__*/react.createElement("div", {
+ className: `stp_tag_picker_tag${duplicateTag === tag ? ` stp_tag_picker_tag_duplicate` : ``}`
+ }, tag, /*#__PURE__*/react.createElement("button", {
+ onClick: () => removeTag(i),
+ className: `stp_tag_picker_tag_remove`
+ }, "X"))), /*#__PURE__*/react.createElement("input", {
+ className: "stp_tag_picker_input",
+ type: "text",
+ onKeyDown: e => handleKeyDown(e),
+ maxlength: "25"
+ })));
+}
+
+/* harmony default export */ const TagPicker_TagPicker = (TagPicker);
;// CONCATENATED MODULE: ./content/panels/js/style-guide/overlay.js
@@ -1200,6 +1263,7 @@ SavedOverlay.prototype = {
+
var StyleGuideOverlay = function (options) {};
StyleGuideOverlay.prototype = {
@@ -1266,6 +1330,10 @@ StyleGuideOverlay.prototype = {
url: "https://example.org",
alt: "Alt Text"
}]
+ }), /*#__PURE__*/react.createElement("h4", {
+ className: "stp_styleguide_h4"
+ }, "TagPicker"), /*#__PURE__*/react.createElement(TagPicker_TagPicker, {
+ tags: [`futurism`, `politics`, `mozilla`]
}), /*#__PURE__*/react.createElement("h3", null, "Typography:"), /*#__PURE__*/react.createElement("h2", {
className: "header_large"
}, ".header_large"), /*#__PURE__*/react.createElement("h3", {
@@ -1524,7 +1592,7 @@ window.pktPanelMessaging = messages;
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module depends on other loaded chunks and execution need to be delayed
-/******/ var __webpack_exports__ = __webpack_require__.O(undefined, [736], () => (__webpack_require__(861)))
+/******/ var __webpack_exports__ = __webpack_require__.O(undefined, [736], () => (__webpack_require__(503)))
/******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
/******/
/******/ })()
diff --git a/browser/components/pocket/content/panels/js/style-guide/overlay.js b/browser/components/pocket/content/panels/js/style-guide/overlay.js
index 8f3abd02f049..370175656f57 100644
--- a/browser/components/pocket/content/panels/js/style-guide/overlay.js
+++ b/browser/components/pocket/content/panels/js/style-guide/overlay.js
@@ -4,6 +4,7 @@ import Header from "../components/Header/Header";
import ArticleList from "../components/ArticleList/ArticleList";
import Button from "../components/Button/Button";
import PopularTopics from "../components/PopularTopics/PopularTopics";
+import TagPicker from "../components/TagPicker/TagPicker";
var StyleGuideOverlay = function(options) {};
@@ -71,6 +72,8 @@ StyleGuideOverlay.prototype = {
},
]}
/>
+ TagPicker
+
Typography:
.header_large
.header_medium
diff --git a/browser/components/pocket/content/panels/style-guide.html b/browser/components/pocket/content/panels/style-guide.html
index dfc88bbe1a00..6a112d322f9e 100644
--- a/browser/components/pocket/content/panels/style-guide.html
+++ b/browser/components/pocket/content/panels/style-guide.html
@@ -9,7 +9,7 @@
Pocket: Style Guide
-
+
diff --git a/browser/components/tests/browser/browser.ini b/browser/components/tests/browser/browser.ini
index 60d8edc226fb..3f911b60ac2f 100644
--- a/browser/components/tests/browser/browser.ini
+++ b/browser/components/tests/browser/browser.ini
@@ -5,8 +5,6 @@ support-files =
[browser_browserGlue_telemetry.js]
[browser_browserGlue_upgradeDialog.js]
-skip-if =
- os == "linux" && bits = 64 # high frequency intermittent Bug 1744379
[browser_browserGlue_upgradeDialog_trigger.js]
[browser_bug538331.js]
skip-if = !updater
diff --git a/browser/config/mozconfigs/linux64/debug-fuzzing b/browser/config/mozconfigs/linux64/debug-fuzzing
index c2ba0f3b01d8..a0e8f39177f0 100644
--- a/browser/config/mozconfigs/linux64/debug-fuzzing
+++ b/browser/config/mozconfigs/linux64/debug-fuzzing
@@ -2,7 +2,7 @@ ac_add_options --enable-debug
. $topsrcdir/build/unix/mozconfig.linux
-export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/llvm-symbolizer"
+export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/bin/llvm-symbolizer"
# Package js shell.
export MOZ_PACKAGE_JSSHELL=1
diff --git a/browser/config/mozconfigs/linux64/debug-fuzzing-noopt b/browser/config/mozconfigs/linux64/debug-fuzzing-noopt
index 090a041d88bb..2c10d92b100e 100644
--- a/browser/config/mozconfigs/linux64/debug-fuzzing-noopt
+++ b/browser/config/mozconfigs/linux64/debug-fuzzing-noopt
@@ -2,7 +2,7 @@ ac_add_options --enable-debug
. $topsrcdir/build/unix/mozconfig.linux
-export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/llvm-symbolizer"
+export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/bin/llvm-symbolizer"
# Package js shell.
export MOZ_PACKAGE_JSSHELL=1
diff --git a/browser/config/mozconfigs/linux64/fuzzing-ccov b/browser/config/mozconfigs/linux64/fuzzing-ccov
index f89e908debbd..4cded3867b16 100644
--- a/browser/config/mozconfigs/linux64/fuzzing-ccov
+++ b/browser/config/mozconfigs/linux64/fuzzing-ccov
@@ -1,6 +1,6 @@
. "$topsrcdir/browser/config/mozconfigs/linux64/code-coverage"
-export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/llvm-symbolizer"
+export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/bin/llvm-symbolizer"
# Even in fuzzing builds without sanitizers, the UBSan runtime is pulled
# in as a dependency to allow libFuzzer to have rudimentary stacks.
diff --git a/browser/modules/AsyncTabSwitcher.jsm b/browser/modules/AsyncTabSwitcher.jsm
index 4cffcd5f812a..50a1c400384e 100644
--- a/browser/modules/AsyncTabSwitcher.jsm
+++ b/browser/modules/AsyncTabSwitcher.jsm
@@ -12,6 +12,7 @@ const { XPCOMUtils } = ChromeUtils.import(
);
XPCOMUtils.defineLazyModuleGetters(this, {
AppConstants: "resource://gre/modules/AppConstants.jsm",
+ PictureInPicture: "resource://gre/modules/PictureInPicture.jsm",
Services: "resource://gre/modules/Services.jsm",
});
@@ -649,8 +650,7 @@ class AsyncTabSwitcher {
let numPending = 0;
let numWarming = 0;
for (let [tab, state] of this.tabState) {
- // Skip print preview browsers since they shouldn't affect tab switching.
- if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
+ if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) {
continue;
}
@@ -726,7 +726,7 @@ class AsyncTabSwitcher {
// Unload any tabs that can be unloaded.
for (let [tab, state] of this.tabState) {
- if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
+ if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) {
continue;
}
@@ -852,8 +852,7 @@ class AsyncTabSwitcher {
onSizeModeOrOcclusionStateChange() {
if (this.minimizedOrFullyOccluded) {
for (let [tab, state] of this.tabState) {
- // Skip print preview browsers since they shouldn't affect tab switching.
- if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
+ if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) {
continue;
}
@@ -915,6 +914,19 @@ class AsyncTabSwitcher {
}
}
+ /**
+ * Check if the browser should be deactivated. If the browser is a print preivew or
+ * PiP browser then we won't deactive it.
+ * @param browser The browser to check if it should be deactivated
+ * @returns false if a print preview or PiP browser else true
+ */
+ shouldDeactivateDocShell(browser) {
+ return !(
+ this.tabbrowser._printPreviewBrowsers.has(browser) ||
+ PictureInPicture.isOriginatingBrowser(browser)
+ );
+ }
+
shouldActivateDocShell(browser) {
let tab = this.tabbrowser.getTabForBrowser(browser);
let state = this.getTabState(tab);
@@ -1220,6 +1232,8 @@ class AsyncTabSwitcher {
let linkedBrowser = tab.linkedBrowser;
let isActive = linkedBrowser && linkedBrowser.docShellIsActive;
let isRendered = linkedBrowser && linkedBrowser.renderLayers;
+ let isPiP =
+ linkedBrowser && PictureInPicture.isOriginatingBrowser(linkedBrowser);
if (tab === this.lastVisibleTab) {
tabString += "V";
@@ -1253,6 +1267,9 @@ class AsyncTabSwitcher {
if (isRendered) {
extraStates += "R";
}
+ if (isPiP) {
+ extraStates += "P";
+ }
if (extraStates != "") {
tabString += `(${extraStates})`;
}
diff --git a/build/build-clang/clang-13.json b/build/build-clang/clang-13.json
index 6f7d9b6921ec..073743eac995 100644
--- a/build/build-clang/clang-13.json
+++ b/build/build-clang/clang-13.json
@@ -1,6 +1,5 @@
{
"patches": [
- "static-llvm-symbolizer_clang_12.patch",
"compiler-rt-cross-compile.patch",
"find_symbolizer_linux_clang_10.patch",
"android-mangling-error_clang_12.patch",
diff --git a/build/build-clang/clang-trunk.json b/build/build-clang/clang-trunk.json
index 536f64eb93bb..b05667dfff7b 100644
--- a/build/build-clang/clang-trunk.json
+++ b/build/build-clang/clang-trunk.json
@@ -1,6 +1,5 @@
{
"patches": [
- "static-llvm-symbolizer_clang_15.patch",
"compiler-rt-cross-compile.patch",
"find_symbolizer_linux_clang_15.patch",
"android-mangling-error_clang_12.patch",
diff --git a/build/build-clang/static-llvm-symbolizer_clang_12.patch b/build/build-clang/static-llvm-symbolizer_clang_12.patch
deleted file mode 100644
index b1fe5d145878..000000000000
--- a/build/build-clang/static-llvm-symbolizer_clang_12.patch
+++ /dev/null
@@ -1,12 +0,0 @@
-diff --git a/llvm/tools/llvm-symbolizer/CMakeLists.txt b/llvm/tools/llvm-symbolizer/CMakeLists.txt
-index c112e344da7..f0f16f1ba2d 100644
---- a/llvm/tools/llvm-symbolizer/CMakeLists.txt
-+++ b/llvm/tools/llvm-symbolizer/CMakeLists.txt
-@@ -18,6 +18,7 @@ set(LLVM_LINK_COMPONENTS
- )
-
- add_llvm_tool(llvm-symbolizer
-+ DISABLE_LLVM_LINK_LLVM_DYLIB
- llvm-symbolizer.cpp
- DEPENDS
- SymbolizerOptsTableGen
diff --git a/build/build-clang/static-llvm-symbolizer_clang_15.patch b/build/build-clang/static-llvm-symbolizer_clang_15.patch
deleted file mode 100644
index 473df64a4e8e..000000000000
--- a/build/build-clang/static-llvm-symbolizer_clang_15.patch
+++ /dev/null
@@ -1,12 +0,0 @@
-diff --git a/llvm/tools/llvm-symbolizer/CMakeLists.txt b/llvm/tools/llvm-symbolizer/CMakeLists.txt
-index f1a6087c0047..faf51c9b5c45 100644
---- a/llvm/tools/llvm-symbolizer/CMakeLists.txt
-+++ b/llvm/tools/llvm-symbolizer/CMakeLists.txt
-@@ -18,6 +18,7 @@ set(LLVM_LINK_COMPONENTS
- )
-
- add_llvm_tool(llvm-symbolizer
-+ DISABLE_LLVM_LINK_LLVM_DYLIB
- llvm-symbolizer.cpp
-
- DEPENDS
diff --git a/build/unix/mozconfig.asan b/build/unix/mozconfig.asan
index e4ec5f6f5776..9c1e7d45e28a 100644
--- a/build/unix/mozconfig.asan
+++ b/build/unix/mozconfig.asan
@@ -1,6 +1,6 @@
. "$topsrcdir/build/unix/mozconfig.unix"
-export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/llvm-symbolizer"
+export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/bin/llvm-symbolizer"
#
# Enable ASan specific code and build workarounds
ac_add_options --enable-address-sanitizer
diff --git a/build/unix/mozconfig.tsan b/build/unix/mozconfig.tsan
index 668f87244e4e..41d742536c0f 100644
--- a/build/unix/mozconfig.tsan
+++ b/build/unix/mozconfig.tsan
@@ -1,6 +1,6 @@
. "$topsrcdir/build/unix/mozconfig.unix"
-export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/llvm-symbolizer"
+export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/bin/llvm-symbolizer"
# Enable TSan specific code and build workarounds
ac_add_options --enable-thread-sanitizer
diff --git a/build/win64/mozconfig.asan b/build/win64/mozconfig.asan
index 7e27407fd979..e9163671f9bd 100644
--- a/build/win64/mozconfig.asan
+++ b/build/win64/mozconfig.asan
@@ -7,7 +7,7 @@ if [ -d "$MOZ_FETCHES_DIR/clang" ]; then
export LDFLAGS="clang_rt.asan_dynamic-x86_64.lib clang_rt.asan_dynamic_runtime_thunk-x86_64.lib"
export MOZ_COPY_PDBS=1
- export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/llvm-symbolizer.exe"
+ export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/bin/llvm-symbolizer.exe"
export MOZ_CLANG_RT_ASAN_LIB_PATH="${CLANG_LIB_DIR}/clang_rt.asan_dynamic-x86_64.dll"
fi
diff --git a/devtools/server/actors/resources/index.js b/devtools/server/actors/resources/index.js
index fccd0383ce67..bdc3783beed2 100644
--- a/devtools/server/actors/resources/index.js
+++ b/devtools/server/actors/resources/index.js
@@ -31,14 +31,21 @@ const TYPES = {
exports.TYPES = TYPES;
// Helper dictionaries, which will contain data specific to each resource type.
-// - `path` is the absolute path to the module defining the Resource Watcher class,
+// - `path` is the absolute path to the module defining the Resource Watcher class.
+//
// Also see the attributes added by `augmentResourceDictionary` for each type:
// - `watchers` is a weak map which will store Resource Watchers
// (i.e. devtools/server/actors/resources/ class instances)
// keyed by target actor -or- watcher actor.
// - `WatcherClass` is a shortcut to the Resource Watcher module.
// Each module exports a Resource Watcher class.
-// These lists are specific for the parent process and each target type.
+//
+// These are several dictionaries, which depend how the resource watcher classes are instantiated.
+
+// Frame target resources are spawned via a BrowsingContext Target Actor.
+// Their watcher class receives a target actor as first argument.
+// They are instantiated for each observed BrowsingContext, from the content process where it runs.
+// They are meant to observe all resources related to a given Browsing Context.
const FrameTargetResources = augmentResourceDictionary({
[TYPES.CACHE_STORAGE]: {
path: "devtools/server/actors/resources/storage-cache",
@@ -92,6 +99,11 @@ const FrameTargetResources = augmentResourceDictionary({
path: "devtools/server/actors/resources/websockets",
},
});
+
+// Process target resources are spawned via a Process Target Actor.
+// Their watcher class receives a process target actor as first argument.
+// They are instantiated for each observed Process (parent and all content processes).
+// They are meant to observe all resources related to a given process.
const ProcessTargetResources = augmentResourceDictionary({
[TYPES.CONSOLE_MESSAGE]: {
path: "devtools/server/actors/resources/console-messages",
@@ -110,6 +122,11 @@ const ProcessTargetResources = augmentResourceDictionary({
},
});
+// Worker target resources are spawned via a Worker Target Actor.
+// Their watcher class receives a worker target actor as first argument.
+// They are instantiated for each observed worker, from the worker thread.
+// They are meant to observe all resources related to a given worker.
+//
// We'll only support a few resource types in Workers (console-message, source,
// thread state, …) as error and platform messages are not supported since we need access
// to Ci, which isn't available in worker context.
@@ -126,6 +143,11 @@ const WorkerTargetResources = augmentResourceDictionary({
},
});
+// Parent process resources are spawned via the Watcher Actor.
+// Their watcher class receives the watcher actor as first argument.
+// They are instantiated once per watcher from the parent process.
+// They are meant to observe all resources related to a given context designated by the Watcher (and its sessionContext)
+// they should be observed from the parent process.
const ParentProcessResources = augmentResourceDictionary({
[TYPES.NETWORK_EVENT]: {
path: "devtools/server/actors/resources/network-events",
@@ -141,6 +163,15 @@ const ParentProcessResources = augmentResourceDictionary({
},
});
+// Root resources are spawned via the Root Actor.
+// Their watcher class receives the root actor as first argument.
+// They are instantiated only once from the parent process.
+// They are meant to observe anything easily observable from the parent process
+// that isn't related to any particular context/target.
+// This is especially useful when you need to observe something without having to instantiate a Watcher actor.
+const RootResources = augmentResourceDictionary({});
+exports.RootResources = RootResources;
+
function augmentResourceDictionary(dict) {
for (const resource of Object.values(dict)) {
resource.watchers = new WeakMap();
@@ -154,15 +185,18 @@ function augmentResourceDictionary(dict) {
* For a given actor, return the related dictionary defined just before,
* that contains info about how to listen for a given resource type, from a given actor.
*
- * @param Actor watcherOrTargetActor
- * Either a WatcherActor or a TargetActor which can be listening to a resource.
+ * @param Actor rootOrWatcherOrTargetActor
+ * Either a RootActor or WatcherActor or a TargetActor which can be listening to a resource.
*/
-function getResourceTypeDictionary(watcherOrTargetActor) {
- const { typeName } = watcherOrTargetActor;
+function getResourceTypeDictionary(rootOrWatcherOrTargetActor) {
+ const { typeName } = rootOrWatcherOrTargetActor;
+ if (typeName == "root") {
+ return RootResources;
+ }
if (typeName == "watcher") {
return ParentProcessResources;
}
- const { targetType } = watcherOrTargetActor;
+ const { targetType } = rootOrWatcherOrTargetActor;
return getResourceTypeDictionaryForTargetType(targetType);
}
@@ -189,16 +223,16 @@ function getResourceTypeDictionaryForTargetType(targetType) {
* For a given actor, return the object stored in one of the previous dictionary
* that contains info about how to listen for a given resource type, from a given actor.
*
- * @param Actor watcherOrTargetActor
- * Either a WatcherActor or a TargetActor which can be listening to a resource.
+ * @param Actor rootOrWatcherOrTargetActor
+ * Either a RootActor or WatcherActor or a TargetActor which can be listening to a resource.
* @param String resourceType
* The resource type to be observed.
*/
-function getResourceTypeEntry(watcherOrTargetActor, resourceType) {
- const dict = getResourceTypeDictionary(watcherOrTargetActor);
+function getResourceTypeEntry(rootOrWatcherOrTargetActor, resourceType) {
+ const dict = getResourceTypeDictionary(rootOrWatcherOrTargetActor);
if (!(resourceType in dict)) {
throw new Error(
- `Unsupported resource type '${resourceType}' for ${watcherOrTargetActor.typeName}`
+ `Unsupported resource type '${resourceType}' for ${rootOrWatcherOrTargetActor.typeName}`
);
}
return dict[resourceType];
@@ -208,43 +242,49 @@ function getResourceTypeEntry(watcherOrTargetActor, resourceType) {
* Start watching for a new list of resource types.
* This will also emit all already existing resources before resolving.
*
- * @param Actor watcherOrTargetActor
- * Either a WatcherActor or a TargetActor which can be listening to a resource.
- * WatcherActor will be used for resources listened from the parent process,
- * and TargetActor will be used for resources listened from the content process.
+ * @param Actor rootOrWatcherOrTargetActor
+ * Either a RootActor or WatcherActor or a TargetActor which can be listening to a resource:
+ * * RootActor will be used for resources observed from the parent process and aren't related to any particular
+ * context/descriptor. They can be observed right away when connecting to the RDP server
+ * without instantiating any actor other than the root actor.
+ * * WatcherActor will be used for resources listened from the parent process.
+ * * TargetActor will be used for resources listened from the content process.
* This actor:
* - defines what context to observe (browsing context, process, worker, ...)
* Via browsingContextID, windows, docShells attributes for the target actor.
* Via the `sessionContext` object for the watcher actor.
+ * (only for Watcher and Target actors. Root actor is context-less.)
* - exposes `notifyResourceAvailable` method to be notified about the available resources
* @param Array resourceTypes
* List of all type of resource to listen to.
*/
-async function watchResources(watcherOrTargetActor, resourceTypes) {
+async function watchResources(rootOrWatcherOrTargetActor, resourceTypes) {
// If we are given a target actor, filter out the resource types supported by the target.
// When using sharedData to pass types between processes, we are passing them for all target types.
- const { targetType } = watcherOrTargetActor;
+ const { targetType } = rootOrWatcherOrTargetActor;
+ // Only target actors usecase will have a target type.
+ // For Root and Watcher we process the `resourceTypes` list unfiltered.
if (targetType) {
resourceTypes = getResourceTypesForTargetType(resourceTypes, targetType);
}
for (const resourceType of resourceTypes) {
const { watchers, WatcherClass } = getResourceTypeEntry(
- watcherOrTargetActor,
+ rootOrWatcherOrTargetActor,
resourceType
);
// Ignore resources we're already listening to
- if (watchers.has(watcherOrTargetActor)) {
+ if (watchers.has(rootOrWatcherOrTargetActor)) {
continue;
}
const watcher = new WatcherClass();
- await watcher.watch(watcherOrTargetActor, {
- onAvailable: watcherOrTargetActor.notifyResourceAvailable,
- onDestroyed: watcherOrTargetActor.notifyResourceDestroyed,
- onUpdated: watcherOrTargetActor.notifyResourceUpdated,
+ await watcher.watch(rootOrWatcherOrTargetActor, {
+ onAvailable: rootOrWatcherOrTargetActor.notifyResourceAvailable,
+ onDestroyed: rootOrWatcherOrTargetActor.notifyResourceDestroyed,
+ onUpdated: rootOrWatcherOrTargetActor.notifyResourceUpdated,
});
- watchers.set(watcherOrTargetActor, watcher);
+ watchers.set(rootOrWatcherOrTargetActor, watcher);
}
}
exports.watchResources = watchResources;
@@ -276,23 +316,23 @@ exports.hasResourceTypesForTargets = hasResourceTypesForTargets;
/**
* Stop watching for a list of resource types.
*
- * @param Actor watcherOrTargetActor
+ * @param Actor rootOrWatcherOrTargetActor
* The related actor, already passed to watchResources.
* @param Array resourceTypes
* List of all type of resource to stop listening to.
*/
-function unwatchResources(watcherOrTargetActor, resourceTypes) {
+function unwatchResources(rootOrWatcherOrTargetActor, resourceTypes) {
for (const resourceType of resourceTypes) {
// Pull all info about this resource type from `Resources` global object
const { watchers } = getResourceTypeEntry(
- watcherOrTargetActor,
+ rootOrWatcherOrTargetActor,
resourceType
);
- const watcher = watchers.get(watcherOrTargetActor);
+ const watcher = watchers.get(rootOrWatcherOrTargetActor);
if (watcher) {
watcher.destroy();
- watchers.delete(watcherOrTargetActor);
+ watchers.delete(rootOrWatcherOrTargetActor);
}
}
}
@@ -301,21 +341,21 @@ exports.unwatchResources = unwatchResources;
/**
* Stop watching for all watched resources on a given actor.
*
- * @param Actor watcherOrTargetActor
+ * @param Actor rootOrWatcherOrTargetActor
* The related actor, already passed to watchResources.
*/
-function unwatchAllTargetResources(watcherOrTargetActor) {
+function unwatchAllResources(rootOrWatcherOrTargetActor) {
for (const { watchers } of Object.values(
- getResourceTypeDictionary(watcherOrTargetActor)
+ getResourceTypeDictionary(rootOrWatcherOrTargetActor)
)) {
- const watcher = watchers.get(watcherOrTargetActor);
+ const watcher = watchers.get(rootOrWatcherOrTargetActor);
if (watcher) {
watcher.destroy();
- watchers.delete(watcherOrTargetActor);
+ watchers.delete(rootOrWatcherOrTargetActor);
}
}
}
-exports.unwatchAllTargetResources = unwatchAllTargetResources;
+exports.unwatchAllResources = unwatchAllResources;
/**
* If we are watching for the given resource type,
diff --git a/devtools/server/actors/root.js b/devtools/server/actors/root.js
index 175aeb8d8c58..deb8e4245a04 100644
--- a/devtools/server/actors/root.js
+++ b/devtools/server/actors/root.js
@@ -18,6 +18,7 @@ const {
const { DevToolsServer } = require("devtools/server/devtools-server");
const protocol = require("devtools/shared/protocol");
const { rootSpec } = require("devtools/shared/specs/root");
+const Resources = require("devtools/server/actors/resources/index");
loader.lazyRequireGetter(
this,
@@ -112,14 +113,28 @@ exports.RootActor = protocol.ActorClassWithSpec(rootSpec, {
this
);
this._onProcessListChanged = this.onProcessListChanged.bind(this);
+ this.notifyResourceAvailable = this.notifyResourceAvailable.bind(this);
+ this.notifyResourceDestroyed = this.notifyResourceDestroyed.bind(this);
+
this._extraActors = {};
this._globalActorPool = new LazyPool(this.conn);
this.applicationType = "browser";
+ // Compute the list of all supported Root Resources
+ const supportedResources = {};
+ for (const resourceType in Resources.RootResources) {
+ supportedResources[resourceType] = true;
+ }
+
this.traits = {
networkMonitor: true,
+
+ // @backward-compat { version 100 } Expose the supported resources.
+ // This traits should be kept, but we can later remove the backward compat comment.
+ resources: supportedResources,
+
// @backward-compat { version 84 } Expose the pref value to the client.
// Services.prefs is undefined in xpcshell tests.
workerConsoleApiMessagesDispatchedToMainThread: Services.prefs
@@ -163,6 +178,8 @@ exports.RootActor = protocol.ActorClassWithSpec(rootSpec, {
* Destroys the actor from the browser window.
*/
destroy: function() {
+ Resources.unwatchAllResources(this);
+
protocol.Actor.prototype.destroy.call(this);
/* Tell the live lists we aren't watching any more. */
@@ -540,6 +557,52 @@ exports.RootActor = protocol.ActorClassWithSpec(rootSpec, {
delete this._extraActors[name];
}
},
+
+ /**
+ * Start watching for a list of resource types.
+ *
+ * See WatcherActor.watchResources.
+ */
+ async watchResources(resourceTypes) {
+ await Resources.watchResources(this, resourceTypes);
+ },
+
+ /**
+ * Stop watching for a list of resource types.
+ *
+ * See WatcherActor.unwatchResources.
+ */
+ unwatchResources(resourceTypes) {
+ Resources.unwatchResources(
+ this,
+ Resources.getParentProcessResourceTypes(resourceTypes)
+ );
+ },
+ /**
+ * Called by Resource Watchers, when new resources are available.
+ *
+ * @param Array resources
+ * List of all available resources. A resource is a JSON object piped over to the client.
+ * It may contain actor IDs, actor forms, to be manually marshalled by the client.
+ */
+ notifyResourceAvailable(resources) {
+ this._emitResourcesForm("resource-available-form", resources);
+ },
+
+ notifyResourceDestroyed(resources) {
+ this._emitResourcesForm("resource-destroyed-form", resources);
+ },
+
+ /**
+ * Wrapper around emit for resource forms.
+ */
+ _emitResourcesForm(name, resources) {
+ if (resources.length === 0) {
+ // Don't try to emit if the resources array is empty.
+ return;
+ }
+ this.emit(name, resources);
+ },
});
/**
diff --git a/devtools/server/actors/targets/content-process.js b/devtools/server/actors/targets/content-process.js
index 12b5872a1445..d0e75e938eae 100644
--- a/devtools/server/actors/targets/content-process.js
+++ b/devtools/server/actors/targets/content-process.js
@@ -213,7 +213,7 @@ const ContentProcessTargetActor = TargetActorMixin(
if (this.isDestroyed()) {
return;
}
- Resources.unwatchAllTargetResources(this);
+ Resources.unwatchAllResources(this);
Actor.prototype.destroy.call(this);
diff --git a/devtools/server/actors/targets/window-global.js b/devtools/server/actors/targets/window-global.js
index 849fc3ae4481..532d9fd4248b 100644
--- a/devtools/server/actors/targets/window-global.js
+++ b/devtools/server/actors/targets/window-global.js
@@ -727,7 +727,7 @@ const windowGlobalTargetPrototype = {
Actor.prototype.destroy.call(this);
TargetActorRegistry.unregisterTargetActor(this);
- Resources.unwatchAllTargetResources(this);
+ Resources.unwatchAllResources(this);
},
/**
diff --git a/devtools/shared/commands/index.js b/devtools/shared/commands/index.js
index 1838d5d89762..8af484230842 100644
--- a/devtools/shared/commands/index.js
+++ b/devtools/shared/commands/index.js
@@ -13,6 +13,8 @@ const Commands = {
"devtools/shared/commands/inspected-window/inspected-window-command",
inspectorCommand: "devtools/shared/commands/inspector/inspector-command",
resourceCommand: "devtools/shared/commands/resource/resource-command",
+ rootResourceCommand:
+ "devtools/shared/commands/root-resource/root-resource-command",
scriptCommand: "devtools/shared/commands/script/script-command",
targetCommand: "devtools/shared/commands/target/target-command",
targetConfigurationCommand:
diff --git a/devtools/shared/commands/moz.build b/devtools/shared/commands/moz.build
index 16c5bfcbf9ed..36bacf281d58 100644
--- a/devtools/shared/commands/moz.build
+++ b/devtools/shared/commands/moz.build
@@ -6,6 +6,7 @@ DIRS += [
"inspected-window",
"inspector",
"resource",
+ "root-resource",
"script",
"target",
"target-configuration",
diff --git a/devtools/shared/commands/root-resource/moz.build b/devtools/shared/commands/root-resource/moz.build
new file mode 100644
index 000000000000..2bf7204d1f9d
--- /dev/null
+++ b/devtools/shared/commands/root-resource/moz.build
@@ -0,0 +1,7 @@
+# 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/.
+
+DevToolsModules(
+ "root-resource-command.js",
+)
diff --git a/devtools/shared/commands/root-resource/root-resource-command.js b/devtools/shared/commands/root-resource/root-resource-command.js
new file mode 100644
index 000000000000..c9934c9ecb46
--- /dev/null
+++ b/devtools/shared/commands/root-resource/root-resource-command.js
@@ -0,0 +1,330 @@
+/* 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";
+
+const { throttle } = require("devtools/shared/throttle");
+
+class RootResourceCommand {
+ /**
+ * This class helps retrieving existing and listening to "root" resources.
+ *
+ * This is a fork of ResourceCommand, but specific to context-less
+ * resources which can be listened to right away when connecting to the RDP server.
+ *
+ * The main difference in term of implementation is that:
+ * - we receive a root front as constructor argument (instead of `commands` object)
+ * - we only listen for RDP events on the Root actor (instead of watcher and target actors)
+ * - there is no legacy listener support
+ * - there is no resource transformers
+ * - there is a lot of logic around targets that is removed here.
+ *
+ * See ResourceCommand for comments and jsdoc.
+ *
+ * TODO Bug 1758530 - Investigate sharing code with ResourceCommand instead of forking.
+ *
+ * @param object commands
+ * The commands object with all interfaces defined from devtools/shared/commands/
+ * @param object rootFront
+ * Front for the Root actor.
+ */
+ constructor({ commands, rootFront }) {
+ this.rootFront = rootFront ? rootFront : commands.client.mainRoot;
+
+ this._onResourceAvailable = this._onResourceAvailable.bind(this);
+ this._onResourceDestroyed = this._onResourceDestroyed.bind(this);
+
+ this._watchers = [];
+
+ this._pendingWatchers = new Set();
+
+ this._cache = [];
+ this._listenedResources = new Set();
+
+ this._processingExistingResources = new Set();
+
+ this._notifyWatchers = this._notifyWatchers.bind(this);
+ this._throttledNotifyWatchers = throttle(this._notifyWatchers, 100);
+ }
+
+ getAllResources(resourceType) {
+ return this._cache.filter(r => r.resourceType === resourceType);
+ }
+
+ getResourceById(resourceType, resourceId) {
+ return this._cache.find(
+ r => r.resourceType === resourceType && r.resourceId === resourceId
+ );
+ }
+
+ async watchResources(resources, options) {
+ const {
+ onAvailable,
+ onUpdated,
+ onDestroyed,
+ ignoreExistingResources = false,
+ } = options;
+
+ if (typeof onAvailable !== "function") {
+ throw new Error(
+ "RootResourceCommand.watchResources expects an onAvailable function as argument"
+ );
+ }
+
+ for (const type of resources) {
+ if (!this._isValidResourceType(type)) {
+ throw new Error(
+ `RootResourceCommand.watchResources invoked with an unknown type: "${type}"`
+ );
+ }
+ }
+
+ const pendingWatcher = {
+ resources,
+ onAvailable,
+ };
+ this._pendingWatchers.add(pendingWatcher);
+
+ if (!this._listenerRegistered) {
+ this._listenerRegistered = true;
+ this.rootFront.on("resource-available-form", this._onResourceAvailable);
+ this.rootFront.on("resource-destroyed-form", this._onResourceDestroyed);
+ }
+
+ const promises = [];
+ for (const resource of resources) {
+ promises.push(this._startListening(resource));
+ }
+ await Promise.all(promises);
+
+ this._notifyWatchers();
+
+ this._pendingWatchers.delete(pendingWatcher);
+
+ const watchedResources = pendingWatcher.resources;
+
+ if (!watchedResources.length) {
+ return;
+ }
+
+ this._watchers.push({
+ resources: watchedResources,
+ onAvailable,
+ onUpdated,
+ onDestroyed,
+ pendingEvents: [],
+ });
+
+ if (!ignoreExistingResources) {
+ await this._forwardExistingResources(watchedResources, onAvailable);
+ }
+ }
+
+ unwatchResources(resources, options) {
+ const { onAvailable } = options;
+
+ if (typeof onAvailable !== "function") {
+ throw new Error(
+ "RootResourceCommand.unwatchResources expects an onAvailable function as argument"
+ );
+ }
+
+ for (const type of resources) {
+ if (!this._isValidResourceType(type)) {
+ throw new Error(
+ `RootResourceCommand.unwatchResources invoked with an unknown type: "${type}"`
+ );
+ }
+ }
+
+ const allWatchers = [...this._watchers, ...this._pendingWatchers];
+ for (const watcherEntry of allWatchers) {
+ if (watcherEntry.onAvailable == onAvailable) {
+ watcherEntry.resources = watcherEntry.resources.filter(resourceType => {
+ return !resources.includes(resourceType);
+ });
+ }
+ }
+ this._watchers = this._watchers.filter(entry => {
+ return entry.resources.length > 0;
+ });
+
+ for (const resource of resources) {
+ const isResourceWatched = allWatchers.some(watcherEntry =>
+ watcherEntry.resources.includes(resource)
+ );
+
+ if (!isResourceWatched && this._listenedResources.has(resource)) {
+ this._stopListening(resource);
+ }
+ }
+ }
+
+ async waitForNextResource(
+ resourceType,
+ { ignoreExistingResources = false, predicate } = {}
+ ) {
+ predicate = predicate || (resource => !!resource);
+
+ let resolve;
+ const promise = new Promise(r => (resolve = r));
+ const onAvailable = async resources => {
+ const matchingResource = resources.find(resource => predicate(resource));
+ if (matchingResource) {
+ this.unwatchResources([resourceType], { onAvailable });
+ resolve(matchingResource);
+ }
+ };
+
+ await this.watchResources([resourceType], {
+ ignoreExistingResources,
+ onAvailable,
+ });
+ return { onResource: promise };
+ }
+
+ async _onResourceAvailable(resources) {
+ for (const resource of resources) {
+ const { resourceType } = resource;
+
+ resource.isAlreadyExistingResource = this._processingExistingResources.has(
+ resourceType
+ );
+
+ this._queueResourceEvent("available", resourceType, resource);
+
+ this._cache.push(resource);
+ }
+
+ this._throttledNotifyWatchers();
+ }
+
+ async _onResourceDestroyed(resources) {
+ for (const resource of resources) {
+ const { resourceType, resourceId } = resource;
+
+ let index = -1;
+ if (resourceId) {
+ index = this._cache.findIndex(
+ cachedResource =>
+ cachedResource.resourceType == resourceType &&
+ cachedResource.resourceId == resourceId
+ );
+ } else {
+ index = this._cache.indexOf(resource);
+ }
+ if (index >= 0) {
+ this._cache.splice(index, 1);
+ } else {
+ console.warn(
+ `Resource ${resourceId || ""} of ${resourceType} was not found.`
+ );
+ }
+
+ this._queueResourceEvent("destroyed", resourceType, resource);
+ }
+ this._throttledNotifyWatchers();
+ }
+
+ _queueResourceEvent(callbackType, resourceType, update) {
+ for (const { resources, pendingEvents } of this._watchers) {
+ if (!resources.includes(resourceType)) {
+ continue;
+ }
+ if (pendingEvents.length > 0) {
+ const lastEvent = pendingEvents[pendingEvents.length - 1];
+ if (lastEvent.callbackType == callbackType) {
+ lastEvent.updates.push(update);
+ continue;
+ }
+ }
+ pendingEvents.push({
+ callbackType,
+ updates: [update],
+ });
+ }
+ }
+
+ _notifyWatchers() {
+ for (const watcherEntry of this._watchers) {
+ const { onAvailable, onDestroyed, pendingEvents } = watcherEntry;
+ watcherEntry.pendingEvents = [];
+
+ for (const { callbackType, updates } of pendingEvents) {
+ try {
+ if (callbackType == "available") {
+ onAvailable(updates, { areExistingResources: false });
+ } else if (callbackType == "destroyed" && onDestroyed) {
+ onDestroyed(updates);
+ }
+ } catch (e) {
+ console.error(
+ "Exception while calling a RootResourceCommand",
+ callbackType,
+ "callback",
+ ":",
+ e
+ );
+ }
+ }
+ }
+ }
+
+ _isValidResourceType(type) {
+ return this.ALL_TYPES.includes(type);
+ }
+
+ async _startListening(resourceType) {
+ if (this._listenedResources.has(resourceType)) {
+ return;
+ }
+ this._listenedResources.add(resourceType);
+
+ this._processingExistingResources.add(resourceType);
+
+ // For now, if the server doesn't support the resource type
+ // act as if we were listening, but do nothing.
+ // Calling watchResources/unwatchResources will work fine,
+ // but no resource will be notified.
+ if (this.rootFront.traits.resources?.[resourceType]) {
+ await this.rootFront.watchResources([resourceType]);
+ }
+ this._processingExistingResources.delete(resourceType);
+ }
+
+ async _forwardExistingResources(resourceTypes, onAvailable) {
+ const existingResources = this._cache.filter(resource =>
+ resourceTypes.includes(resource.resourceType)
+ );
+ if (existingResources.length > 0) {
+ await onAvailable(existingResources, { areExistingResources: true });
+ }
+ }
+
+ _stopListening(resourceType) {
+ if (!this._listenedResources.has(resourceType)) {
+ throw new Error(
+ `Stopped listening for resource '${resourceType}' that isn't being listened to`
+ );
+ }
+ this._listenedResources.delete(resourceType);
+
+ this._cache = this._cache.filter(
+ cachedResource => cachedResource.resourceType !== resourceType
+ );
+
+ if (
+ !this.rootFront.isDestroyed() &&
+ this.rootFront.traits.resources?.[resourceType]
+ ) {
+ this.rootFront.unwatchResources([resourceType]);
+ }
+ }
+}
+
+RootResourceCommand.TYPES = RootResourceCommand.prototype.TYPES = {};
+RootResourceCommand.ALL_TYPES = RootResourceCommand.prototype.ALL_TYPES = Object.values(
+ RootResourceCommand.TYPES
+);
+module.exports = RootResourceCommand;
diff --git a/devtools/shared/specs/root.js b/devtools/shared/specs/root.js
index aa2d6607a808..f1b1c5e7d9a7 100644
--- a/devtools/shared/specs/root.js
+++ b/devtools/shared/specs/root.js
@@ -80,6 +80,20 @@ const rootSpecPrototype = {
},
},
+ watchResources: {
+ request: {
+ resourceTypes: Arg(0, "array:string"),
+ },
+ response: {},
+ },
+
+ unwatchResources: {
+ request: {
+ resourceTypes: Arg(0, "array:string"),
+ },
+ oneway: true,
+ },
+
requestTypes: {
request: {},
response: RetVal("json"),
@@ -105,6 +119,15 @@ const rootSpecPrototype = {
processListChanged: {
type: "processListChanged",
},
+
+ "resource-available-form": {
+ type: "resource-available-form",
+ resources: Arg(0, "array:json"),
+ },
+ "resource-destroyed-form": {
+ type: "resource-destroyed-form",
+ resources: Arg(0, "array:json"),
+ },
},
};
diff --git a/dom/base/RemoteOuterWindowProxy.cpp b/dom/base/RemoteOuterWindowProxy.cpp
index 6c8738f2b39e..aa561c7e6ddf 100644
--- a/dom/base/RemoteOuterWindowProxy.cpp
+++ b/dom/base/RemoteOuterWindowProxy.cpp
@@ -8,6 +8,7 @@
#include "js/Proxy.h"
#include "mozilla/Maybe.h"
#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/ProxyHandlerUtils.h"
#include "mozilla/dom/RemoteObjectProxy.h"
#include "mozilla/dom/WindowBinding.h"
#include "mozilla/dom/WindowProxyHolder.h"
diff --git a/dom/base/Selection.cpp b/dom/base/Selection.cpp
index f74341ceee48..db6c605960b7 100644
--- a/dom/base/Selection.cpp
+++ b/dom/base/Selection.cpp
@@ -97,12 +97,12 @@ static constexpr nsLiteralCString kNoDocumentTypeNodeError =
static constexpr nsLiteralCString kNoRangeExistsError =
"No selection range exists"_ns;
+namespace mozilla {
+
/******************************************************************************
* Utility methods defined in nsISelectionController.idl
******************************************************************************/
-namespace mozilla {
-
const char* ToChar(SelectionType aSelectionType) {
switch (aSelectionType) {
case SelectionType::eInvalid:
@@ -134,6 +134,48 @@ const char* ToChar(SelectionType aSelectionType) {
}
}
+/******************************************************************************
+ * Utility methods defined in nsISelectionListener.idl
+ ******************************************************************************/
+
+nsCString SelectionChangeReasonsToCString(int16_t aReasons) {
+ nsCString reasons;
+ if (!aReasons) {
+ reasons.AssignLiteral("NO_REASON");
+ return reasons;
+ }
+ auto EnsureSeparator = [](nsCString& aString) -> void {
+ if (!aString.IsEmpty()) {
+ aString.AppendLiteral(" | ");
+ }
+ };
+ struct ReasonData {
+ int16_t mReason;
+ const char* mReasonStr;
+
+ ReasonData(int16_t aReason, const char* aReasonStr)
+ : mReason(aReason), mReasonStr(aReasonStr) {}
+ };
+ for (const ReasonData& reason :
+ {ReasonData(nsISelectionListener::DRAG_REASON, "DRAG_REASON"),
+ ReasonData(nsISelectionListener::MOUSEDOWN_REASON, "MOUSEDOWN_REASON"),
+ ReasonData(nsISelectionListener::MOUSEUP_REASON, "MOUSEUP_REASON"),
+ ReasonData(nsISelectionListener::KEYPRESS_REASON, "KEYPRESS_REASON"),
+ ReasonData(nsISelectionListener::SELECTALL_REASON, "SELECTALL_REASON"),
+ ReasonData(nsISelectionListener::COLLAPSETOSTART_REASON,
+ "COLLAPSETOSTART_REASON"),
+ ReasonData(nsISelectionListener::COLLAPSETOEND_REASON,
+ "COLLAPSETOEND_REASON"),
+ ReasonData(nsISelectionListener::IME_REASON, "IME_REASON"),
+ ReasonData(nsISelectionListener::JS_REASON, "JS_REASON")}) {
+ if (aReasons & reason.mReason) {
+ EnsureSeparator(reasons);
+ reasons.Append(reason.mReasonStr);
+ }
+ }
+ return reasons;
+}
+
} // namespace mozilla
//#define DEBUG_SELECTION // uncomment for printf describing every collapse and
@@ -3180,17 +3222,17 @@ void Selection::NotifySelectionListeners() {
}
}
-void Selection::StartBatchChanges() {
+void Selection::StartBatchChanges(const char* aDetails) {
if (mFrameSelection) {
RefPtr frameSelection = mFrameSelection;
- frameSelection->StartBatchChanges();
+ frameSelection->StartBatchChanges(aDetails);
}
}
-void Selection::EndBatchChanges(int16_t aReason) {
+void Selection::EndBatchChanges(const char* aDetails, int16_t aReasons) {
if (mFrameSelection) {
RefPtr frameSelection = mFrameSelection;
- frameSelection->EndBatchChanges(aReason);
+ frameSelection->EndBatchChanges(aDetails, aReasons);
}
}
@@ -3403,7 +3445,7 @@ void Selection::SetBaseAndExtentInternal(InLimiter aInLimiter,
// after we set the direction.
// XXX If they are disconnected, shouldn't we return error before allocating
// new nsRange instance?
- SelectionBatcher batch(this);
+ SelectionBatcher batch(this, __FUNCTION__);
const Maybe order =
nsContentUtils::ComparePoints(aAnchorRef, aFocusRef);
if (order && (*order <= 0)) {
@@ -3442,7 +3484,7 @@ void Selection::SetStartAndEndInternal(InLimiter aInLimiter,
}
// Don't fire "selectionchange" event until everything done.
- SelectionBatcher batch(this);
+ SelectionBatcher batch(this, __FUNCTION__);
if (aInLimiter == InLimiter::eYes) {
if (!mFrameSelection ||
diff --git a/dom/base/Selection.h b/dom/base/Selection.h
index 38904d167752..f2033ed49c15 100644
--- a/dom/base/Selection.h
+++ b/dom/base/Selection.h
@@ -74,12 +74,25 @@ class Selection final : public nsSupportsWeakReference,
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Selection)
- // match this up with EndbatchChanges. will stop ui updates while multiple
- // selection methods are called
- void StartBatchChanges();
+ /**
+ * Match this up with EndbatchChanges. will stop ui updates while multiple
+ * selection methods are called
+ *
+ * @param aDetails string to explian why this is called. This won't be
+ * stored nor exposed to selection listeners etc. Just for logging.
+ */
+ void StartBatchChanges(const char* aDetails);
- // match this up with StartBatchChanges
- void EndBatchChanges(int16_t aReason = nsISelectionListener::NO_REASON);
+ /**
+ * Match this up with StartBatchChanges
+ *
+ * @param aDetails string to explian why this is called. This won't be
+ * stored nor exposed to selection listeners etc. Just for logging.
+ * @param aReasons potentially multiple of the reasons defined in
+ * nsISelectionListener.idl
+ */
+ void EndBatchChanges(const char* aDetails,
+ int16_t aReason = nsISelectionListener::NO_REASON);
/**
* NotifyAutoCopy() starts to notify AutoCopyListener of selection changes.
@@ -938,24 +951,36 @@ class Selection final : public nsSupportsWeakReference,
// Stack-class to turn on/off selection batching.
class MOZ_STACK_CLASS SelectionBatcher final {
private:
- RefPtr mSelection;
- int16_t mReason;
+ const RefPtr mSelection;
+ const int16_t mReasons;
+ const char* const mRequesterFuncName;
public:
- explicit SelectionBatcher(Selection& aSelectionRef)
- : SelectionBatcher(&aSelectionRef) {}
+ /**
+ * @param aRequesterFuncName function name which wants the selection batch.
+ * This won't be stored nor exposed to selection listeners etc, used only for
+ * logging. This MUST be living when the destructor runs.
+ */
+ // TODO: Mark these constructors `MOZ_CAN_RUN_SCRIPT` because the destructor
+ // may run script via nsISelectionListener.
+ explicit SelectionBatcher(Selection& aSelectionRef,
+ const char* aRequesterFuncName,
+ int16_t aReasons = nsISelectionListener::NO_REASON)
+ : SelectionBatcher(&aSelectionRef, aRequesterFuncName, aReasons) {}
explicit SelectionBatcher(Selection* aSelection,
- int16_t aReason = nsISelectionListener::NO_REASON) {
- mSelection = aSelection;
- mReason = aReason;
+ const char* aRequesterFuncName,
+ int16_t aReasons = nsISelectionListener::NO_REASON)
+ : mSelection(aSelection),
+ mReasons(aReasons),
+ mRequesterFuncName(aRequesterFuncName) {
if (mSelection) {
- mSelection->StartBatchChanges();
+ mSelection->StartBatchChanges(mRequesterFuncName);
}
}
~SelectionBatcher() {
if (mSelection) {
- mSelection->EndBatchChanges(mReason);
+ mSelection->EndBatchChanges(mRequesterFuncName, mReasons);
}
}
};
diff --git a/dom/base/WindowNamedPropertiesHandler.cpp b/dom/base/WindowNamedPropertiesHandler.cpp
index 03b6343dcb97..7601438af6b5 100644
--- a/dom/base/WindowNamedPropertiesHandler.cpp
+++ b/dom/base/WindowNamedPropertiesHandler.cpp
@@ -6,6 +6,7 @@
#include "WindowNamedPropertiesHandler.h"
#include "mozilla/dom/EventTargetBinding.h"
+#include "mozilla/dom/ProxyHandlerUtils.h"
#include "mozilla/dom/WindowBinding.h"
#include "mozilla/dom/WindowProxyHolder.h"
#include "nsContentUtils.h"
diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp
index c5700544ed53..48974006615c 100644
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -111,7 +111,6 @@
#include "mozilla/dom/ContentFrameMessageManager.h"
#include "mozilla/dom/ContentMediaController.h"
#include "mozilla/dom/CustomElementRegistry.h"
-#include "mozilla/dom/DOMJSProxyHandler.h"
#include "mozilla/dom/DebuggerNotification.h"
#include "mozilla/dom/DebuggerNotificationBinding.h"
#include "mozilla/dom/DebuggerNotificationManager.h"
@@ -151,6 +150,7 @@
#include "mozilla/dom/PopupBlocker.h"
#include "mozilla/dom/PrimitiveConversions.h"
#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ProxyHandlerUtils.h"
#include "mozilla/dom/RootedDictionary.h"
#include "mozilla/dom/ScriptLoader.h"
#include "mozilla/dom/ScriptSettings.h"
diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp
index 6b3602c736e6..25253594da31 100644
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -39,6 +39,7 @@
#include "mozilla/dom/Storage.h"
#include "mozilla/dom/MaybeCrossOriginObject.h"
#include "mozilla/dom/Performance.h"
+#include "mozilla/dom/ProxyHandlerUtils.h"
#include "mozilla/dom/StorageEvent.h"
#include "mozilla/dom/StorageEventBinding.h"
#include "mozilla/dom/StorageNotifierService.h"
diff --git a/dom/base/nsISelectionListener.idl b/dom/base/nsISelectionListener.idl
index 4aed15d1a0f0..ed195145c63f 100644
--- a/dom/base/nsISelectionListener.idl
+++ b/dom/base/nsISelectionListener.idl
@@ -29,4 +29,13 @@ interface nsISelectionListener : nsISupports
in short reason);
};
+%{C++
+namespace mozilla {
+/**
+ * Returning names of `nsISelectionListener::*_REASON` in aReasons.
+ */
+nsCString SelectionChangeReasonsToCString(int16_t aReasons);
+
+} // namespace mozilla
+%}
diff --git a/dom/bindings/BindingUtils.cpp b/dom/bindings/BindingUtils.cpp
index cab10064d034..46dbda52bf27 100644
--- a/dom/bindings/BindingUtils.cpp
+++ b/dom/bindings/BindingUtils.cpp
@@ -65,6 +65,7 @@
#include "mozilla/dom/HTMLElementBinding.h"
#include "mozilla/dom/HTMLEmbedElementBinding.h"
#include "mozilla/dom/MaybeCrossOriginObject.h"
+#include "mozilla/dom/ObservableArrayProxyHandler.h"
#include "mozilla/dom/ReportingUtils.h"
#include "mozilla/dom/XULElementBinding.h"
#include "mozilla/dom/XULFrameElementBinding.h"
@@ -3505,11 +3506,11 @@ nsresult UnwrapWindowProxyArg(JSContext* cx, JS::Handle src,
return NS_OK;
}
-template
-bool GetMaplikeSetlikeBackingObject(JSContext* aCx, JS::Handle aObj,
- size_t aSlotIndex,
- JS::MutableHandle aBackingObj,
- bool* aBackingObjCreated) {
+template
+static bool GetBackingObject(JSContext* aCx, JS::Handle aObj,
+ size_t aSlotIndex,
+ JS::MutableHandle aBackingObj,
+ bool* aBackingObjCreated, Args... aArgs) {
JS::Rooted reflector(aCx);
reflector = IsDOMObject(aObj)
? aObj
@@ -3526,7 +3527,7 @@ bool GetMaplikeSetlikeBackingObject(JSContext* aCx, JS::Handle aObj,
{
JSAutoRealm ar(aCx, reflector);
JS::Rooted newBackingObj(aCx);
- newBackingObj.set(Method(aCx));
+ newBackingObj.set(Method(aCx, aArgs...));
if (NS_WARN_IF(!newBackingObj)) {
return false;
}
@@ -3549,16 +3550,35 @@ bool GetMaplikeBackingObject(JSContext* aCx, JS::Handle aObj,
size_t aSlotIndex,
JS::MutableHandle aBackingObj,
bool* aBackingObjCreated) {
- return GetMaplikeSetlikeBackingObject(
- aCx, aObj, aSlotIndex, aBackingObj, aBackingObjCreated);
+ return GetBackingObject(aCx, aObj, aSlotIndex, aBackingObj,
+ aBackingObjCreated);
}
bool GetSetlikeBackingObject(JSContext* aCx, JS::Handle aObj,
size_t aSlotIndex,
JS::MutableHandle aBackingObj,
bool* aBackingObjCreated) {
- return GetMaplikeSetlikeBackingObject(
- aCx, aObj, aSlotIndex, aBackingObj, aBackingObjCreated);
+ return GetBackingObject(aCx, aObj, aSlotIndex, aBackingObj,
+ aBackingObjCreated);
+}
+
+static inline JSObject* NewObservableArrayProxyObject(
+ JSContext* aCx, const ObservableArrayProxyHandler* aHandler) {
+ JS::RootedObject target(aCx, JS::NewArrayObject(aCx, 0));
+ if (NS_WARN_IF(!target)) {
+ return nullptr;
+ }
+
+ JS::RootedValue targetValue(aCx, JS::ObjectValue(*target));
+ return js::NewProxyObject(aCx, aHandler, targetValue, nullptr);
+}
+
+bool GetObservableArrayBackingObject(
+ JSContext* aCx, JS::Handle aObj, size_t aSlotIndex,
+ JS::MutableHandle aBackingObj, bool* aBackingObjCreated,
+ const ObservableArrayProxyHandler* aHandler) {
+ return GetBackingObject(
+ aCx, aObj, aSlotIndex, aBackingObj, aBackingObjCreated, aHandler);
}
bool ForEachHandler(JSContext* aCx, unsigned aArgc, JS::Value* aVp) {
diff --git a/dom/bindings/BindingUtils.h b/dom/bindings/BindingUtils.h
index e1064fb9afd2..4f48889d1d72 100644
--- a/dom/bindings/BindingUtils.h
+++ b/dom/bindings/BindingUtils.h
@@ -62,6 +62,7 @@ class CustomElementReactionsStack;
class Document;
class EventTarget;
class MessageManagerGlobal;
+class ObservableArrayProxyHandler;
class DedicatedWorkerGlobalScope;
template
class Record;
@@ -370,6 +371,13 @@ MOZ_ALWAYS_INLINE nsresult UnwrapObject(JSObject* obj, OwningNonNull& value,
obj, value, PrototypeID, PrototypeTraits::Depth, cx);
}
+template
+MOZ_ALWAYS_INLINE nsresult UnwrapObject(JSObject* obj, NonNull& value,
+ const CxType& cx) {
+ return binding_detail::UnwrapObjectInternal(
+ obj, value, PrototypeID, PrototypeTraits::Depth, cx);
+}
+
// An UnwrapObject overload that just calls one of the JSObject* ones.
template
MOZ_ALWAYS_INLINE nsresult UnwrapObject(JS::Handle obj, U& value,
@@ -378,6 +386,13 @@ MOZ_ALWAYS_INLINE nsresult UnwrapObject(JS::Handle obj, U& value,
return UnwrapObject(&obj.toObject(), value, cx);
}
+template
+MOZ_ALWAYS_INLINE nsresult UnwrapObject(JS::Handle obj,
+ NonNull& value, const CxType& cx) {
+ MOZ_ASSERT(obj.isObject());
+ return UnwrapObject(&obj.toObject(), value, cx);
+}
+
template
MOZ_ALWAYS_INLINE void AssertStaticUnwrapOK() {
static_assert(PrototypeID != prototypes::id::Window,
@@ -3104,6 +3119,16 @@ bool GetSetlikeBackingObject(JSContext* aCx, JS::Handle aObj,
JS::MutableHandle aBackingObj,
bool* aBackingObjCreated);
+// Unpacks backing object (ES Proxy exotic object) from the reserved slot of a
+// reflector for a observableArray attribute. If backing object does not exist,
+// creates backing object in the compartment of the reflector involved, making
+// this safe to use across compartments/via xrays. Return values of these
+// methods will always be in the context compartment.
+bool GetObservableArrayBackingObject(
+ JSContext* aCx, JS::Handle aObj, size_t aSlotIndex,
+ JS::MutableHandle aBackingObj, bool* aBackingObjCreated,
+ const ObservableArrayProxyHandler* aHandler);
+
// Get the desired prototype object for an object construction from the given
// CallArgs. The CallArgs must be for a constructor call. The
// aProtoId/aCreator arguments are used to get a default if we don't find a
diff --git a/dom/bindings/Codegen.py b/dom/bindings/Codegen.py
index e3682e16af9b..9bcc1d4ca510 100644
--- a/dom/bindings/Codegen.py
+++ b/dom/bindings/Codegen.py
@@ -214,6 +214,7 @@ def idlTypeNeedsCallContext(type, descriptor=None, allowTreatNonCallableAsNull=F
or type.isCallback()
or type.isDictionary()
or type.isRecord()
+ or type.isObservableArray()
):
# These can all throw if a primitive is passed in, at the very least.
# There are some rare cases when we know we have an object, but those
@@ -1467,6 +1468,8 @@ class CGHeaders(CGWrapper):
# Also add headers for the type the record is
# parametrized over, if needed.
addHeadersForType((t.inner, dictionary))
+ elif unrolled.isObservableArray():
+ bindingHeaders.add("mozilla/dom/ObservableArrayProxyHandler.h")
for t in getAllTypes(
descriptors + callbackDescriptors, dictionaries, callbacks
@@ -2145,6 +2148,20 @@ def finalizeHook(descriptor, hookName, gcx, obj):
""",
obj=obj,
)
+ for m in descriptor.interface.members:
+ if m.isAttr() and m.type.isObservableArray():
+ finalize += fill(
+ """
+ {
+ JS::Value val = JS::GetReservedSlot(obj, ${slot});
+ if (!val.isUndefined()) {
+ JSObject* obj = &val.toObject();
+ js::SetProxyReservedSlot(obj, OBSERVABLE_ARRAY_DOM_INTERFACE_SLOT, JS::UndefinedValue());
+ }
+ }
+ """,
+ slot=memberReservedSlot(m, descriptor),
+ )
if descriptor.wrapperCache:
finalize += "ClearWrapper(self, self, %s);\n" % obj
if descriptor.isGlobal():
@@ -5855,15 +5872,16 @@ def getJSToNativeConversionInfo(
assert not (isEnforceRange and isClamp) # These are mutually exclusive
- if type.isSequence():
+ if type.isSequence() or type.isObservableArray():
assert not isEnforceRange and not isClamp and not isAllowShared
if failureCode is None:
notSequence = (
- 'cx.ThrowErrorMessage("%s", "sequence");\n'
+ 'cx.ThrowErrorMessage("%s", "%s");\n'
"%s"
% (
firstCap(sourceDescription),
+ "sequence" if type.isSequence() else "observable array",
exceptionCode,
)
)
@@ -8350,6 +8368,12 @@ def getWrapTemplateForType(
# NB: setObject{,OrNull}(..., some-object-type) calls JS_WrapValue(), so is fallible
return (head + setter(toValue % result, wrapAsType=type), False)
+ if type.isObservableArray():
+ # This first argument isn't used at all for now, the attribute getter
+ # for ObservableArray type are generated in getObservableArrayGetterBody
+ # instead.
+ return "", False
+
if not (
type.isUnion()
or type.isPrimitive()
@@ -9509,6 +9533,9 @@ class CGPerSignatureCall(CGThing):
idlNode.identifier.name,
)
)
+ elif idlNode.isAttr() and idlNode.type.isObservableArray():
+ assert setter
+ cgThings.append(CGObservableArraySetterGenerator(descriptor, idlNode))
else:
context = GetLabelForErrorReporting(descriptor, idlNode, isConstructor)
if getter:
@@ -10482,17 +10509,15 @@ class CGSetterCall(CGPerSignatureCall):
def wrap_return_value(self):
attr = self.idlNode
+ clearSlot = ""
if self.descriptor.wrapperCache and attr.slotIndices is not None:
if attr.getExtendedAttribute("StoreInSlot"):
- args = "cx, self"
- else:
+ clearSlot = "%s(cx, self);\n" % MakeClearCachedValueNativeName(
+ self.idlNode
+ )
+ elif attr.getExtendedAttribute("Cached"):
args = "self"
- clearSlot = "%s(%s);\n" % (
- MakeClearCachedValueNativeName(self.idlNode),
- args,
- )
- else:
- clearSlot = ""
+ clearSlot = "%s(self);\n" % MakeClearCachedValueNativeName(self.idlNode)
# We have no return value
return "\n" "%s" "return true;\n" % clearSlot
@@ -11210,6 +11235,13 @@ class CGSpecializedGetter(CGAbstractStaticMethod):
return prefix + getMaplikeOrSetlikeSizeGetterBody(
self.descriptor, self.attr
)
+
+ if self.attr.type.isObservableArray():
+ assert not self.attr.getExtendedAttribute("CrossOriginReadable")
+ # If the attribute is observableArray, due to having to unpack the
+ # backing object from the slot, this requires its own generator.
+ return prefix + getObservableArrayGetterBody(self.descriptor, self.attr)
+
nativeName = CGSpecializedGetter.makeNativeName(self.descriptor, self.attr)
type = self.attr.type
if self.attr.getExtendedAttribute("CrossOriginReadable"):
@@ -11457,6 +11489,7 @@ class CGSpecializedSetter(CGAbstractStaticMethod):
nativeType=self.descriptor.nativeType,
call=call,
)
+
return prefix + fill(
"""
auto* self = static_cast<${nativeType}*>(void_self);
@@ -11809,8 +11842,13 @@ class CGMemberJITInfo(CGThing):
and infallibleForMember(self.member, self.member.type, self.descriptor)
)
isAlwaysInSlot = self.member.getExtendedAttribute("StoreInSlot")
+
if self.member.slotIndices is not None:
- assert isAlwaysInSlot or self.member.getExtendedAttribute("Cached")
+ assert (
+ isAlwaysInSlot
+ or self.member.getExtendedAttribute("Cached")
+ or self.member.type.isObservableArray()
+ )
isLazilyCachedInSlot = not isAlwaysInSlot
slotIndex = memberReservedSlot(self.member, self.descriptor)
# We'll statically assert that this is not too big in
@@ -12034,6 +12072,8 @@ class CGMemberJITInfo(CGThing):
)
if t.isDictionary():
return "JSVAL_TYPE_OBJECT"
+ if t.isObservableArray():
+ return "JSVAL_TYPE_OBJECT"
if not t.isPrimitive():
raise TypeError("No idea what type " + str(t) + " is.")
tag = t.tag()
@@ -15745,18 +15785,20 @@ class CGDOMJSProxyHandler_getElements(ClassMethod):
)
-class CGDOMJSProxyHandler_getInstance(ClassMethod):
- def __init__(self):
+class CGJSProxyHandler_getInstance(ClassMethod):
+ def __init__(self, type):
+ self.type = type
ClassMethod.__init__(
- self, "getInstance", "const DOMProxyHandler*", [], static=True
+ self, "getInstance", "const %s*" % self.type, [], static=True
)
def getBody(self):
- return dedent(
+ return fill(
"""
- static const DOMProxyHandler instance;
+ static const ${type} instance;
return &instance;
- """
+ """,
+ type=self.type,
)
@@ -16048,7 +16090,7 @@ class CGDOMJSProxyHandler(CGClass):
CGDOMJSProxyHandler_className(descriptor),
CGDOMJSProxyHandler_finalizeInBackground(descriptor),
CGDOMJSProxyHandler_finalize(descriptor),
- CGDOMJSProxyHandler_getInstance(),
+ CGJSProxyHandler_getInstance("DOMProxyHandler"),
CGDOMJSProxyHandler_delete(descriptor),
]
constructors = [
@@ -16245,6 +16287,11 @@ class CGDescriptor(CGThing):
elif m.isMaplikeOrSetlike():
cgThings.append(CGMaplikeOrSetlikeHelperGenerator(descriptor, m))
elif m.isAttr():
+ if m.type.isObservableArray():
+ cgThings.append(
+ CGObservableArrayProxyHandlerGenerator(descriptor, m)
+ )
+ cgThings.append(CGObservableArrayHelperGenerator(descriptor, m))
if m.getExtendedAttribute("Unscopable"):
assert not m.isStatic()
unscopableNames.append(m.identifier.name)
@@ -18126,11 +18173,19 @@ class CGBindingRoot(CGThing):
return any(hasCrossOriginProperty(m) for m in desc.interface.members)
+ def descriptorHasObservableArrayTypes(desc):
+ def hasObservableArrayTypes(m):
+ return m.isAttr() and m.type.isObservableArray()
+
+ return any(hasObservableArrayTypes(m) for m in desc.interface.members)
+
bindingDeclareHeaders["mozilla/dom/RemoteObjectProxy.h"] = any(
descriptorHasCrossOriginProperties(d) for d in descriptors
)
bindingDeclareHeaders["jsapi.h"] = any(
- descriptorHasCrossOriginProperties(d) for d in descriptors
+ descriptorHasCrossOriginProperties(d)
+ or descriptorHasObservableArrayTypes(d)
+ for d in descriptors
)
bindingDeclareHeaders["js/TypeDecls.h"] = not bindingDeclareHeaders["jsapi.h"]
bindingDeclareHeaders["js/RootingAPI.h"] = not bindingDeclareHeaders["jsapi.h"]
@@ -18194,6 +18249,10 @@ class CGBindingRoot(CGThing):
d.concrete and d.proxy for d in descriptors
)
+ bindingHeaders["mozilla/dom/ProxyHandlerUtils.h"] = any(
+ d.concrete and d.proxy for d in descriptors
+ )
+
bindingHeaders["js/String.h"] = any(
d.needsMissingPropUseCounters for d in descriptors
)
@@ -19145,7 +19204,7 @@ class CGBindingImplClass(CGClass):
if not m.isStatic() or not skipStaticMethods:
appendMethod(m)
elif m.isAttr():
- if m.isMaplikeOrSetlikeAttr():
+ if m.isMaplikeOrSetlikeAttr() or m.type.isObservableArray():
# Handled by generated code already
continue
self.methodDecls.append(cgGetter(descriptor, m))
@@ -19308,6 +19367,42 @@ class CGBindingImplClass(CGClass):
return self._deps
+class CGExampleObservableArrayCallback(CGNativeMember):
+ def __init__(self, descriptor, attr, callbackName):
+ assert attr.isAttr()
+ assert attr.type.isObservableArray()
+ CGNativeMember.__init__(
+ self,
+ descriptor,
+ attr,
+ self.makeNativeName(attr, callbackName),
+ (
+ BuiltinTypes[IDLBuiltinType.Types.void],
+ [
+ FakeArgument(attr.type.inner, "aValue"),
+ FakeArgument(
+ BuiltinTypes[IDLBuiltinType.Types.unsigned_long], "aIndex"
+ ),
+ ],
+ ),
+ ["needsErrorResult"],
+ )
+
+ def declare(self, cgClass):
+ assert self.member.isAttr()
+ assert self.member.type.isObservableArray()
+ return CGNativeMember.declare(self, cgClass)
+
+ def define(self, cgClass):
+ return ""
+
+ @staticmethod
+ def makeNativeName(attr, callbackName):
+ assert attr.isAttr()
+ nativeName = MakeNativeName(attr.identifier.name)
+ return "On" + callbackName + nativeName
+
+
class CGExampleClass(CGBindingImplClass):
"""
Codegen for the actual example class implementation for this descriptor
@@ -19365,6 +19460,15 @@ class CGExampleClass(CGBindingImplClass):
else:
decorators = "final"
+ for m in descriptor.interface.members:
+ if m.isAttr() and m.type.isObservableArray():
+ self.methodDecls.append(
+ CGExampleObservableArrayCallback(descriptor, m, "Set")
+ )
+ self.methodDecls.append(
+ CGExampleObservableArrayCallback(descriptor, m, "Delete")
+ )
+
CGClass.__init__(
self,
self.nativeLeafName(descriptor),
@@ -21451,7 +21555,7 @@ class CGMaplikeOrSetlikeMethodGenerator(CGThing):
def appendBoolResult(self):
if self.helperImpl:
- return ([CGGeneric()], ["&aRetVal"], [])
+ return ([CGGeneric("bool retVal;\n")], ["&retVal"], [])
return ([CGGeneric("bool result;\n")], ["&result"], [])
def forEach(self):
@@ -21528,7 +21632,7 @@ class CGMaplikeOrSetlikeMethodGenerator(CGThing):
code = []
# We don't need to create the result variable because it'll be created elsewhere
# for JSObject Get method
- if not self.helperImpl or not self.helperImpl.handleJSObjectGetHelper():
+ if not self.helperImpl or not self.helperImpl.needsScopeBody():
code = [
CGGeneric(
dedent(
@@ -21597,19 +21701,20 @@ class CGMaplikeOrSetlikeMethodGenerator(CGThing):
return self.cgRoot.define()
-class CGMaplikeOrSetlikeHelperFunctionGenerator(CallbackMember):
+class CGHelperFunctionGenerator(CallbackMember):
"""
- Generates code to allow C++ to perform operations on backing objects. Gets
- a context from the binding wrapper, turns arguments into JS::Values (via
+ Generates code to allow C++ to perform operations. Gets a context from the
+ binding wrapper, turns arguments into JS::Values (via
CallbackMember/CGNativeMember argument conversion), then uses
- CGMaplikeOrSetlikeMethodGenerator to generate the body.
-
+ getCall to generate the body for getting result, and maybe convert the
+ result into return type (via CallbackMember/CGNativeMember result
+ conversion)
"""
class HelperFunction(CGAbstractMethod):
"""
Generates context retrieval code and rooted JSObject for interface for
- CGMaplikeOrSetlikeMethodGenerator to use
+ method generator to use
"""
def __init__(self, descriptor, name, args, code, returnType):
@@ -21622,31 +21727,14 @@ class CGMaplikeOrSetlikeHelperFunctionGenerator(CallbackMember):
def __init__(
self,
descriptor,
- maplikeOrSetlike,
name,
- needsKeyArg=False,
- needsValueArg=False,
- needsValueTypeReturn=False,
- needsBoolReturn=False,
+ args,
+ returnType=BuiltinTypes[IDLBuiltinType.Types.void],
+ needsResultConversion=True,
):
- assert not (needsValueTypeReturn and needsBoolReturn)
- args = []
- self.maplikeOrSetlike = maplikeOrSetlike
- self.needsBoolReturn = needsBoolReturn
- self.needsValueTypeReturn = needsValueTypeReturn
+ assert returnType.isType()
+ self.needsResultConversion = needsResultConversion
- returnType = (
- BuiltinTypes[IDLBuiltinType.Types.void]
- if not self.needsValueTypeReturn
- else maplikeOrSetlike.valueType
- )
-
- if needsKeyArg:
- args.append(FakeArgument(maplikeOrSetlike.keyType, "aKey"))
- if needsValueArg:
- assert needsKeyArg
- assert not needsValueTypeReturn
- args.append(FakeArgument(maplikeOrSetlike.valueType, "aValue"))
# Run CallbackMember init function to generate argument conversion code.
# wrapScope is set to 'obj' when generating maplike or setlike helper
# functions, as we don't have access to the CallbackPreserveColor
@@ -21658,26 +21746,21 @@ class CGMaplikeOrSetlikeHelperFunctionGenerator(CallbackMember):
descriptor,
False,
wrapScope="obj",
- passJSBitsAsNeeded=self.handleJSObjectGetHelper(),
+ passJSBitsAsNeeded=typeNeedsCx(returnType),
)
- if self.needsValueTypeReturn:
- finalReturnType = self.returnType
- elif needsBoolReturn:
- finalReturnType = "bool"
- else:
- finalReturnType = "void"
# Wrap CallbackMember body code into a CGAbstractMethod to make
# generation easier.
- self.implMethod = CGMaplikeOrSetlikeHelperFunctionGenerator.HelperFunction(
- descriptor, name, self.args, self.body, finalReturnType
+ self.implMethod = CGHelperFunctionGenerator.HelperFunction(
+ descriptor, name, self.args, self.body, self.returnType
)
def getCallSetup(self):
- # If handleJSObjectGetHelper is true, it means the caller will provide a JSContext,
- # so we don't need to create JSContext and enter UnprivilegedJunkScopeOrWorkerGlobal here.
+ # If passJSBitsAsNeeded is true, it means the caller will provide a
+ # JSContext, so we don't need to create JSContext and enter
+ # UnprivilegedJunkScopeOrWorkerGlobal here.
code = "MOZ_ASSERT(self);\n"
- if not self.handleJSObjectGetHelper():
+ if not self.passJSBitsAsNeeded:
code += dedent(
"""
AutoJSAPI jsapi;
@@ -21711,21 +21794,12 @@ class CGMaplikeOrSetlikeHelperFunctionGenerator(CallbackMember):
% self.getDefaultRetval()
)
- # For the JSObject Get method, we'd like wrap the inner code in a scope such that
- # the code can use the same realm. So here we are creating the result variable
- # outside of the scope.
- if self.handleJSObjectGetHelper():
- code += dedent(
- """
- JS::Rooted result(cx);
- """
- )
- else:
- code += dedent(
- """
- JSAutoRealm reflectorRealm(cx, obj);
- """
- )
+ # We'd like wrap the inner code in a scope such that the code can use the
+ # same realm. So here we are creating the result variable outside of the
+ # scope.
+ if self.needsScopeBody():
+ code += "JS::Rooted result(cx);\n"
+
return code
def getArgs(self, returnType, argList):
@@ -21735,20 +21809,15 @@ class CGMaplikeOrSetlikeHelperFunctionGenerator(CallbackMember):
return [Argument(self.descriptorProvider.nativeType + "*", "self")] + args
def needsScopeBody(self):
- return self.handleJSObjectGetHelper()
+ return self.passJSBitsAsNeeded
def getArgvDeclFailureCode(self):
return "aRv.Throw(NS_ERROR_UNEXPECTED);\n"
- def handleJSObjectGetHelper(self):
- return self.needsValueTypeReturn and self.maplikeOrSetlike.valueType.isObject()
-
def getResultConversion(self):
- if self.needsBoolReturn:
- return "return aRetVal;\n"
- elif self.needsValueTypeReturn:
+ if self.needsResultConversion:
code = ""
- if self.handleJSObjectGetHelper():
+ if self.needsScopeBody():
code = dedent(
"""
if (!JS_WrapValue(cx, &result)) {
@@ -21761,7 +21830,7 @@ class CGMaplikeOrSetlikeHelperFunctionGenerator(CallbackMember):
failureCode = dedent("aRv.Throw(NS_ERROR_UNEXPECTED);\nreturn nullptr;\n")
exceptionCode = None
- if self.maplikeOrSetlike.valueType.isPrimitive():
+ if self.retvalType.isPrimitive():
exceptionCode = dedent(
"aRv.NoteJSContextException(cx);\nreturn%s;\n"
% self.getDefaultRetval()
@@ -21774,34 +21843,26 @@ class CGMaplikeOrSetlikeHelperFunctionGenerator(CallbackMember):
isDefinitelyObject=True,
exceptionCode=exceptionCode,
)
- return "return;\n"
+
+ assignRetval = string.Template(
+ self.getRetvalInfo(self.retvalType, False)[2]
+ ).substitute(
+ {
+ "declName": "retVal",
+ }
+ )
+ return assignRetval
def getRvalDecl(self):
- if self.needsBoolReturn:
- return "bool aRetVal;\n"
- elif self.handleJSObjectGetHelper():
- return "JSAutoRealm reflectorRealm(cx, obj);\n"
- return ""
+ # hack to make sure we put JSAutoRealm inside the body scope
+ return "JSAutoRealm reflectorRealm(cx, obj);\n"
def getArgcDecl(self):
# Don't need argc for anything.
return None
- def getDefaultRetval(self):
- if self.needsBoolReturn:
- return " false"
- elif self.needsValueTypeReturn:
- return CallbackMember.getDefaultRetval(self)
- return ""
-
def getCall(self):
- return CGMaplikeOrSetlikeMethodGenerator(
- self.descriptorProvider,
- self.maplikeOrSetlike,
- self.name.lower(),
- self.needsValueTypeReturn,
- helperImpl=self,
- ).define()
+ assert False # Override me!
def getPrettyName(self):
return self.name
@@ -21813,6 +21874,61 @@ class CGMaplikeOrSetlikeHelperFunctionGenerator(CallbackMember):
return self.implMethod.define()
+class CGMaplikeOrSetlikeHelperFunctionGenerator(CGHelperFunctionGenerator):
+ """
+ Generates code to allow C++ to perform operations on backing objects. Gets
+ a context from the binding wrapper, turns arguments into JS::Values (via
+ CallbackMember/CGNativeMember argument conversion), then uses
+ CGMaplikeOrSetlikeMethodGenerator to generate the body.
+ """
+
+ def __init__(
+ self,
+ descriptor,
+ maplikeOrSetlike,
+ name,
+ needsKeyArg=False,
+ needsValueArg=False,
+ needsValueTypeReturn=False,
+ needsBoolReturn=False,
+ needsResultConversion=True,
+ ):
+ self.maplikeOrSetlike = maplikeOrSetlike
+ self.needsValueTypeReturn = needsValueTypeReturn
+
+ args = []
+ if needsKeyArg:
+ args.append(FakeArgument(maplikeOrSetlike.keyType, "aKey"))
+ if needsValueArg:
+ assert needsKeyArg
+ assert not needsValueTypeReturn
+ args.append(FakeArgument(maplikeOrSetlike.valueType, "aValue"))
+
+ returnType = BuiltinTypes[IDLBuiltinType.Types.void]
+ if needsBoolReturn:
+ returnType = BuiltinTypes[IDLBuiltinType.Types.boolean]
+ elif needsValueTypeReturn:
+ returnType = maplikeOrSetlike.valueType
+
+ CGHelperFunctionGenerator.__init__(
+ self,
+ descriptor,
+ name,
+ args,
+ returnType,
+ needsResultConversion,
+ )
+
+ def getCall(self):
+ return CGMaplikeOrSetlikeMethodGenerator(
+ self.descriptorProvider,
+ self.maplikeOrSetlike,
+ self.name.lower(),
+ self.needsValueTypeReturn,
+ helperImpl=self,
+ ).define()
+
+
class CGMaplikeOrSetlikeHelperGenerator(CGNamespace):
"""
Declares and defines convenience methods for accessing backing objects on
@@ -21839,6 +21955,7 @@ class CGMaplikeOrSetlikeHelperGenerator(CGNamespace):
"Delete",
needsKeyArg=True,
needsBoolReturn=True,
+ needsResultConversion=False,
),
CGMaplikeOrSetlikeHelperFunctionGenerator(
descriptor,
@@ -21846,6 +21963,7 @@ class CGMaplikeOrSetlikeHelperGenerator(CGNamespace):
"Has",
needsKeyArg=True,
needsBoolReturn=True,
+ needsResultConversion=False,
),
]
if self.maplikeOrSetlike.isMaplike():
@@ -21936,6 +22054,648 @@ class CGIterableMethodGenerator(CGGeneric):
)
+def getObservableArrayBackingObject(descriptor, attr, errorReturn="return false;\n"):
+ """
+ Generate code to get/create a JS backing list for an observableArray attribute
+ from the declaration slot.
+ """
+ assert attr.isAttr()
+ assert attr.type.isObservableArray()
+
+ return fill(
+ """
+ JS::Rooted backingObj(cx);
+ bool created = false;
+ if (!GetObservableArrayBackingObject(cx, obj, ${slot},
+ &backingObj, &created, ${namespace}::ObservableArrayProxyHandler::getInstance())) {
+ $*{errorReturn}
+ }
+ if (created) {
+ PreserveWrapper(self);
+ js::SetProxyReservedSlot(backingObj,
+ OBSERVABLE_ARRAY_DOM_INTERFACE_SLOT,
+ JS::PrivateValue(self));
+ }
+ """,
+ namespace=toBindingNamespace(MakeNativeName(attr.identifier.name)),
+ slot=memberReservedSlot(attr, descriptor),
+ errorReturn=errorReturn,
+ selfType=descriptor.nativeType,
+ )
+
+
+def getObservableArrayGetterBody(descriptor, attr):
+ """
+ Creates the body for the getter method of an observableArray attribute.
+ """
+ assert attr.type.isObservableArray()
+ return fill(
+ """
+ if (xpc::WrapperFactory::IsXrayWrapper(obj)) {
+ JS_ReportErrorASCII(cx, "Accessing from Xray wrapper is not supported.");
+ return false;
+ }
+ $*{getBackingObj}
+ MOZ_ASSERT(!JS_IsExceptionPending(cx));
+ args.rval().setObject(*backingObj);
+ return true;
+ """,
+ getBackingObj=getObservableArrayBackingObject(descriptor, attr),
+ )
+
+
+class CGObservableArrayProxyHandler_callback(ClassMethod):
+ """
+ Base class for declaring and defining the hook methods for ObservableArrayProxyHandler
+ subclasses to get the interface native object from backing object and calls
+ its On{Set|Delete}* callback.
+
+ * 'callbackType': "Set" or "Delete".
+ * 'invalidTypeFatal' (optional): If True, we don't expect the type
+ conversion would fail, so generate the
+ assertion code if type conversion fails.
+ """
+
+ def __init__(
+ self, descriptor, attr, name, args, callbackType, invalidTypeFatal=False
+ ):
+ assert attr.isAttr()
+ assert attr.type.isObservableArray()
+ assert callbackType in ["Set", "Delete"]
+ self.descriptor = descriptor
+ self.attr = attr
+ self.innertype = attr.type.inner
+ self.callbackType = callbackType
+ self.invalidTypeFatal = invalidTypeFatal
+ ClassMethod.__init__(
+ self,
+ name,
+ "bool",
+ args,
+ visibility="protected",
+ virtual=True,
+ override=True,
+ const=True,
+ )
+
+ def preConversion(self):
+ """
+ The code to run before the conversion steps.
+ """
+ return ""
+
+ def preCallback(self):
+ """
+ The code to run before calling the callback.
+ """
+ return ""
+
+ def postCallback(self):
+ """
+ The code to run after calling the callback, all subclasses should override
+ this to generate the return values.
+ """
+ assert False # Override me!
+
+ def getBody(self):
+ exceptionCode = (
+ fill(
+ """
+ MOZ_ASSERT_UNREACHABLE("The item in ObservableArray backing list is not ${innertype}?");
+ return false;
+ """,
+ innertype=self.innertype,
+ )
+ if self.invalidTypeFatal
+ else None
+ )
+ convertType = instantiateJSToNativeConversion(
+ getJSToNativeConversionInfo(
+ self.innertype,
+ self.descriptor,
+ sourceDescription="Element in ObservableArray backing list",
+ exceptionCode=exceptionCode,
+ ),
+ {
+ "declName": "decl",
+ "holderName": "holder",
+ "val": "aValue",
+ },
+ )
+ callbackArgs = ["decl", "aIndex", "rv"]
+ if typeNeedsCx(self.innertype):
+ callbackArgs.insert(0, "cx")
+ return fill(
+ """
+ MOZ_ASSERT(IsObservableArrayProxy(aProxy));
+ $*{preConversion}
+
+ BindingCallContext cx(aCx, "ObservableArray ${name}");
+ $*{convertType}
+
+ $*{preCallback}
+ JS::Value val = js::GetProxyReservedSlot(aProxy, OBSERVABLE_ARRAY_DOM_INTERFACE_SLOT);
+ auto* interface = static_cast<${ifaceType}*>(val.toPrivate());
+ MOZ_ASSERT(interface);
+
+ ErrorResult rv;
+ MOZ_KnownLive(interface)->${methodName}(${callbackArgs});
+ $*{postCallback}
+ """,
+ preConversion=self.preConversion(),
+ name=self.name,
+ convertType=convertType.define(),
+ preCallback=self.preCallback(),
+ ifaceType=self.descriptor.nativeType,
+ methodName="On%s%s"
+ % (self.callbackType, MakeNativeName(self.attr.identifier.name)),
+ callbackArgs=", ".join(callbackArgs),
+ postCallback=self.postCallback(),
+ )
+
+
+class CGObservableArrayProxyHandler_OnDeleteItem(
+ CGObservableArrayProxyHandler_callback
+):
+ """
+ Declares and defines the hook methods for ObservableArrayProxyHandler
+ subclasses to get the interface native object from backing object and calls
+ its OnDelete* callback.
+ """
+
+ def __init__(self, descriptor, attr):
+ args = [
+ Argument("JSContext*", "aCx"),
+ Argument("JS::HandleObject", "aProxy"),
+ Argument("JS::HandleValue", "aValue"),
+ Argument("uint32_t", "aIndex"),
+ ]
+ CGObservableArrayProxyHandler_callback.__init__(
+ self,
+ descriptor,
+ attr,
+ "OnDeleteItem",
+ args,
+ "Delete",
+ True,
+ )
+
+ def postCallback(self):
+ return dedent(
+ """
+ return !rv.MaybeSetPendingException(cx);
+ """
+ )
+
+
+class CGObservableArrayProxyHandler_SetIndexedValue(
+ CGObservableArrayProxyHandler_callback
+):
+ """
+ Declares and defines the hook methods for ObservableArrayProxyHandler
+ subclasses to run the setting the indexed value steps.
+ """
+
+ def __init__(self, descriptor, attr):
+ args = [
+ Argument("JSContext*", "aCx"),
+ Argument("JS::HandleObject", "aProxy"),
+ Argument("JS::HandleObject", "aBackingList"),
+ Argument("uint32_t", "aIndex"),
+ Argument("JS::HandleValue", "aValue"),
+ Argument("JS::ObjectOpResult&", "aResult"),
+ ]
+ CGObservableArrayProxyHandler_callback.__init__(
+ self,
+ descriptor,
+ attr,
+ "SetIndexedValue",
+ args,
+ "Set",
+ )
+
+ def preConversion(self):
+ return dedent(
+ """
+ uint32_t oldLen;
+ if (!JS::GetArrayLength(aCx, aBackingList, &oldLen)) {
+ return false;
+ }
+
+ if (aIndex > oldLen) {
+ return aResult.failBadIndex();
+ }
+ """
+ )
+
+ def preCallback(self):
+ return dedent(
+ """
+ if (aIndex < oldLen) {
+ JS::RootedValue value(aCx);
+ if (!JS_GetElement(aCx, aBackingList, aIndex, &value)) {
+ return false;
+ }
+
+ if (!OnDeleteItem(aCx, aProxy, value, aIndex)) {
+ return false;
+ }
+ }
+
+ """
+ )
+
+ def postCallback(self):
+ return dedent(
+ """
+ if (rv.MaybeSetPendingException(cx)) {
+ return false;
+ }
+
+ if (!JS_SetElement(aCx, aBackingList, aIndex, aValue)) {
+ return false;
+ }
+
+ return aResult.succeed();
+ """
+ )
+
+
+class CGObservableArrayProxyHandler(CGThing):
+ """
+ A class for declaring a ObservableArrayProxyHandler.
+ """
+
+ def __init__(self, descriptor, attr):
+ assert attr.isAttr()
+ assert attr.type.isObservableArray()
+ methods = [
+ # The item stored in backing object should always be converted successfully.
+ CGObservableArrayProxyHandler_OnDeleteItem(descriptor, attr),
+ CGObservableArrayProxyHandler_SetIndexedValue(descriptor, attr),
+ CGJSProxyHandler_getInstance("ObservableArrayProxyHandler"),
+ ]
+ parentClass = "mozilla::dom::ObservableArrayProxyHandler"
+ self.proxyHandler = CGClass(
+ "ObservableArrayProxyHandler",
+ bases=[ClassBase(parentClass)],
+ constructors=[],
+ methods=methods,
+ )
+
+ def declare(self):
+ # Our class declaration should happen when we're defining
+ return ""
+
+ def define(self):
+ return self.proxyHandler.declare() + "\n" + self.proxyHandler.define()
+
+
+class CGObservableArrayProxyHandlerGenerator(CGNamespace):
+ """
+ Declares and defines convenience methods for accessing backing list objects
+ for observable array attribute. Generates function signatures, un/packs
+ backing list objects from slot, etc.
+ """
+
+ def __init__(self, descriptor, attr):
+ assert attr.isAttr()
+ assert attr.type.isObservableArray()
+ namespace = toBindingNamespace(MakeNativeName(attr.identifier.name))
+ proxyHandler = CGObservableArrayProxyHandler(descriptor, attr)
+ CGNamespace.__init__(self, namespace, proxyHandler)
+
+
+class CGObservableArraySetterGenerator(CGGeneric):
+ """
+ Creates setter for an observableArray attributes.
+ """
+
+ def __init__(self, descriptor, attr):
+ assert attr.isAttr()
+ assert attr.type.isObservableArray()
+ getBackingObject = getObservableArrayBackingObject(descriptor, attr)
+ setElement = dedent(
+ """
+ if (!JS_SetElement(cx, backingObj, i, val)) {
+ return false;
+ }
+ """
+ )
+ conversion = wrapForType(
+ attr.type.inner,
+ descriptor,
+ {
+ "result": "arg0.ElementAt(i)",
+ "successCode": setElement,
+ "jsvalRef": "val",
+ "jsvalHandle": "&val",
+ },
+ )
+ CGGeneric.__init__(
+ self,
+ fill(
+ """
+ if (xpc::WrapperFactory::IsXrayWrapper(obj)) {
+ JS_ReportErrorASCII(cx, "Accessing from Xray wrapper is not supported.");
+ return false;
+ }
+
+ ${getBackingObject}
+ const ObservableArrayProxyHandler* handler = GetObservableArrayProxyHandler(backingObj);
+ if (!handler->SetLength(cx, backingObj, 0)) {
+ return false;
+ }
+
+ JS::Rooted val(cx);
+ for (size_t i = 0; i < arg0.Length(); i++) {
+ $*{conversion}
+ }
+ """,
+ conversion=conversion,
+ getBackingObject=getBackingObject,
+ ),
+ )
+
+
+class CGObservableArrayHelperFunctionGenerator(CGHelperFunctionGenerator):
+ """
+ Generates code to allow C++ to perform operations on backing objects. Gets
+ a context from the binding wrapper, turns arguments into JS::Values (via
+ CallbackMember/CGNativeMember argument conversion), then uses
+ MethodBodyGenerator to generate the body.
+ """
+
+ class MethodBodyGenerator(CGThing):
+ """
+ Creates methods body for observable array attribute. It is expected that all
+ methods will be have a maplike/setlike object attached. Unwrapping/wrapping
+ will be taken care of by the usual method generation machinery in
+ CGMethodCall/CGPerSignatureCall. Functionality is filled in here instead of
+ using CGCallGenerator.
+ """
+
+ def __init__(
+ self,
+ descriptor,
+ attr,
+ methodName,
+ helperGenerator,
+ needsIndexArg,
+ ):
+ assert attr.isAttr()
+ assert attr.type.isObservableArray()
+
+ CGThing.__init__(self)
+ self.helperGenerator = helperGenerator
+ self.cgRoot = CGList([])
+
+ self.cgRoot.append(
+ CGGeneric(
+ getObservableArrayBackingObject(
+ descriptor,
+ attr,
+ dedent(
+ """
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return%s;
+ """
+ % helperGenerator.getDefaultRetval()
+ ),
+ )
+ )
+ )
+
+ # Generates required code for the method. Method descriptions included
+ # in definitions below. Throw if we don't have a method to fill in what
+ # we're looking for.
+ try:
+ methodGenerator = getattr(self, methodName)
+ except AttributeError:
+ raise TypeError(
+ "Missing observable array method definition '%s'" % methodName
+ )
+ # Method generator returns tuple, containing:
+ #
+ # - a list of CGThings representing setup code for preparing to call
+ # the JS API function
+ # - JS API function name
+ # - a list of arguments needed for the JS API function we're calling
+ # - a list of CGThings representing code needed before return.
+ (setupCode, funcName, arguments, returnCode) = methodGenerator()
+
+ # Append the list of setup code CGThings
+ self.cgRoot.append(CGList(setupCode))
+ # Create the JS API call
+ if needsIndexArg:
+ arguments.insert(0, "aIndex")
+ self.cgRoot.append(
+ CGWrapper(
+ CGGeneric(
+ fill(
+ """
+ aRv.MightThrowJSException();
+ if (!${funcName}(${args})) {
+ aRv.StealExceptionFromJSContext(cx);
+ return${retval};
+ }
+ """,
+ funcName=funcName,
+ args=", ".join(["cx", "backingObj"] + arguments),
+ retval=helperGenerator.getDefaultRetval(),
+ )
+ )
+ )
+ )
+ # Append code before return
+ self.cgRoot.append(CGList(returnCode))
+
+ def elementat(self):
+ setupCode = []
+ if not self.helperGenerator.needsScopeBody():
+ setupCode.append(CGGeneric("JS::Rooted result(cx);\n"))
+ returnCode = [
+ CGGeneric(
+ fill(
+ """
+ if (result.isUndefined()) {
+ aRv.Throw(NS_ERROR_NOT_AVAILABLE);
+ return${retval};
+ }
+ """,
+ retval=self.helperGenerator.getDefaultRetval(),
+ )
+ )
+ ]
+ return (setupCode, "JS_GetElement", ["&result"], returnCode)
+
+ def replaceelementat(self):
+ setupCode = [
+ CGGeneric(
+ fill(
+ """
+ uint32_t length;
+ aRv.MightThrowJSException();
+ if (!JS::GetArrayLength(cx, backingObj, &length)) {
+ aRv.StealExceptionFromJSContext(cx);
+ return${retval};
+ }
+ if (aIndex > length) {
+ aRv.ThrowRangeError("Invalid index");
+ return${retval};
+ }
+ """,
+ retval=self.helperGenerator.getDefaultRetval(),
+ )
+ )
+ ]
+ return (setupCode, "JS_SetElement", ["argv[0]"], [])
+
+ def appendelement(self):
+ setupCode = [
+ CGGeneric(
+ fill(
+ """
+ uint32_t length;
+ aRv.MightThrowJSException();
+ if (!JS::GetArrayLength(cx, backingObj, &length)) {
+ aRv.StealExceptionFromJSContext(cx);
+ return${retval};
+ }
+ """,
+ retval=self.helperGenerator.getDefaultRetval(),
+ )
+ )
+ ]
+ return (setupCode, "JS_SetElement", ["length", "argv[0]"], [])
+
+ def removelastelement(self):
+ setupCode = [
+ CGGeneric(
+ fill(
+ """
+ uint32_t length;
+ aRv.MightThrowJSException();
+ if (!JS::GetArrayLength(cx, backingObj, &length)) {
+ aRv.StealExceptionFromJSContext(cx);
+ return${retval};
+ }
+ if (length == 0) {
+ aRv.Throw(NS_ERROR_NOT_AVAILABLE);
+ return${retval};
+ }
+ """,
+ retval=self.helperGenerator.getDefaultRetval(),
+ )
+ )
+ ]
+ return (setupCode, "JS::SetArrayLength", ["length - 1"], [])
+
+ def length(self):
+ return (
+ [CGGeneric("uint32_t retVal;\n")],
+ "JS::GetArrayLength",
+ ["&retVal"],
+ [],
+ )
+
+ def define(self):
+ return self.cgRoot.define()
+
+ def __init__(
+ self,
+ descriptor,
+ attr,
+ name,
+ returnType=BuiltinTypes[IDLBuiltinType.Types.void],
+ needsResultConversion=True,
+ needsIndexArg=False,
+ needsValueArg=False,
+ ):
+ assert attr.isAttr()
+ assert attr.type.isObservableArray()
+ self.attr = attr
+ self.needsIndexArg = needsIndexArg
+
+ args = []
+ if needsValueArg:
+ args.append(FakeArgument(attr.type.inner, "aValue"))
+
+ CGHelperFunctionGenerator.__init__(
+ self,
+ descriptor,
+ name,
+ args,
+ returnType,
+ needsResultConversion,
+ )
+
+ def getArgs(self, returnType, argList):
+ if self.needsIndexArg:
+ argList = [
+ FakeArgument(BuiltinTypes[IDLBuiltinType.Types.unsigned_long], "aIndex")
+ ] + argList
+ return CGHelperFunctionGenerator.getArgs(self, returnType, argList)
+
+ def getCall(self):
+ return CGObservableArrayHelperFunctionGenerator.MethodBodyGenerator(
+ self.descriptorProvider,
+ self.attr,
+ self.name.lower(),
+ self,
+ self.needsIndexArg,
+ ).define()
+
+
+class CGObservableArrayHelperGenerator(CGNamespace):
+ """
+ Declares and defines convenience methods for accessing backing object for
+ observable array type. Generates function signatures, un/packs
+ backing objects from slot, etc.
+ """
+
+ def __init__(self, descriptor, attr):
+ assert attr.isAttr()
+ assert attr.type.isObservableArray()
+
+ namespace = "%sHelpers" % MakeNativeName(attr.identifier.name)
+ helpers = [
+ CGObservableArrayHelperFunctionGenerator(
+ descriptor,
+ attr,
+ "ElementAt",
+ returnType=attr.type.inner,
+ needsIndexArg=True,
+ ),
+ CGObservableArrayHelperFunctionGenerator(
+ descriptor,
+ attr,
+ "ReplaceElementAt",
+ needsIndexArg=True,
+ needsValueArg=True,
+ ),
+ CGObservableArrayHelperFunctionGenerator(
+ descriptor,
+ attr,
+ "AppendElement",
+ needsValueArg=True,
+ ),
+ CGObservableArrayHelperFunctionGenerator(
+ descriptor,
+ attr,
+ "RemoveLastElement",
+ ),
+ CGObservableArrayHelperFunctionGenerator(
+ descriptor,
+ attr,
+ "Length",
+ returnType=BuiltinTypes[IDLBuiltinType.Types.unsigned_long],
+ needsResultConversion=False,
+ ),
+ ]
+ CGNamespace.__init__(self, namespace, CGList(helpers, "\n"))
+
+
class GlobalGenRoots:
"""
Roots for global codegen.
diff --git a/dom/bindings/DOMJSProxyHandler.h b/dom/bindings/DOMJSProxyHandler.h
index a2f9ebf72d0e..619381bf3323 100644
--- a/dom/bindings/DOMJSProxyHandler.h
+++ b/dom/bindings/DOMJSProxyHandler.h
@@ -7,20 +7,12 @@
#ifndef mozilla_dom_DOMJSProxyHandler_h
#define mozilla_dom_DOMJSProxyHandler_h
-#include "mozilla/Attributes.h"
-#include "mozilla/Likely.h"
+#include "mozilla/Assertions.h"
#include "mozilla/Maybe.h"
-#include "mozilla/TextUtils.h"
#include "jsapi.h"
#include "js/Object.h" // JS::GetClass
#include "js/Proxy.h"
-#include "js/String.h" // JS::AtomToLinearString, JS::GetLinearString{CharAt,Length}
-#include "nsString.h"
-
-// XXX Avoid including this (and maybe some of those above by moving inline
-// function bodies out)
-#include "jsfriendapi.h"
namespace mozilla {
namespace dom {
@@ -173,42 +165,6 @@ inline const DOMProxyHandler* GetDOMProxyHandler(JSObject* obj) {
return static_cast(js::GetProxyHandler(obj));
}
-extern jsid s_length_id;
-
-// A return value of UINT32_MAX indicates "not an array index". Note, in
-// particular, that UINT32_MAX itself is not a valid array index in general.
-inline uint32_t GetArrayIndexFromId(JS::Handle id) {
- // Much like js::IdIsIndex, except with a fast path for "length" and another
- // fast path for starting with a lowercase ascii char. Is that second one
- // really needed? I guess it is because StringIsArrayIndex is out of line...
- // as of now, use id.get() instead of id otherwise operands mismatch error
- // occurs.
- if (MOZ_LIKELY(id.isInt())) {
- return id.toInt();
- }
- if (MOZ_LIKELY(id.get() == s_length_id)) {
- return UINT32_MAX;
- }
- if (MOZ_UNLIKELY(!id.isAtom())) {
- return UINT32_MAX;
- }
-
- JSLinearString* str = JS::AtomToLinearString(id.toAtom());
- if (MOZ_UNLIKELY(JS::GetLinearStringLength(str) == 0)) {
- return UINT32_MAX;
- }
-
- char16_t firstChar = JS::GetLinearStringCharAt(str, 0);
- if (MOZ_LIKELY(IsAsciiLowercaseAlpha(firstChar))) {
- return UINT32_MAX;
- }
-
- uint32_t i;
- return js::StringIsArrayIndex(str, &i) ? i : UINT32_MAX;
-}
-
-inline bool IsArrayIndex(uint32_t index) { return index < UINT32_MAX; }
-
} // namespace dom
} // namespace mozilla
diff --git a/dom/bindings/JSSlots.h b/dom/bindings/JSSlots.h
index 5e19957d63ca..5b9d91ccf315 100644
--- a/dom/bindings/JSSlots.h
+++ b/dom/bindings/JSSlots.h
@@ -31,4 +31,11 @@
// slot for the unforgeable holder is needed.
#define DOM_INTERFACE_PROTO_SLOTS_BASE 0
+// The slot index of raw pointer of dom object stored in observable array exotic
+// object. We nedd this in order to call the OnSet* and OnDelete* callbacks.
+#define OBSERVABLE_ARRAY_DOM_INTERFACE_SLOT 0
+
+// The slot index of backing list stored in observable array exotic object.
+#define OBSERVABLE_ARRAY_BACKING_LIST_OBJECT_SLOT 1
+
#endif /* mozilla_dom_DOMSlots_h */
diff --git a/dom/bindings/ObservableArrayProxyHandler.cpp b/dom/bindings/ObservableArrayProxyHandler.cpp
new file mode 100644
index 000000000000..7c8f727e195b
--- /dev/null
+++ b/dom/bindings/ObservableArrayProxyHandler.cpp
@@ -0,0 +1,366 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "mozilla/dom/ObservableArrayProxyHandler.h"
+
+#include "jsapi.h"
+#include "js/friend/ErrorMessages.h"
+#include "js/Conversions.h"
+#include "js/Object.h"
+#include "mozilla/dom/JSSlots.h"
+#include "mozilla/dom/ProxyHandlerUtils.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/ErrorResult.h"
+#include "nsDebug.h"
+#include "nsJSUtils.h"
+
+namespace mozilla::dom {
+
+const char ObservableArrayProxyHandler::family = 0;
+
+bool ObservableArrayProxyHandler::defineProperty(
+ JSContext* aCx, JS::HandleObject aProxy, JS::HandleId aId,
+ JS::Handle aDesc,
+ JS::ObjectOpResult& aResult) const {
+ if (aId.get() == s_length_id) {
+ if (aDesc.isAccessorDescriptor()) {
+ return aResult.failNotDataDescriptor();
+ }
+ if (aDesc.hasConfigurable() && aDesc.configurable()) {
+ return aResult.failInvalidDescriptor();
+ }
+ if (aDesc.hasEnumerable() && aDesc.enumerable()) {
+ return aResult.failInvalidDescriptor();
+ }
+ if (aDesc.hasWritable() && !aDesc.writable()) {
+ return aResult.failInvalidDescriptor();
+ }
+ if (aDesc.hasValue()) {
+ JS::RootedObject backingListObj(aCx);
+ if (!GetBackingListObject(aCx, aProxy, &backingListObj)) {
+ return false;
+ }
+
+ return SetLength(aCx, aProxy, backingListObj, aDesc.value(), aResult);
+ }
+ return aResult.succeed();
+ }
+ uint32_t index = GetArrayIndexFromId(aId);
+ if (IsArrayIndex(index)) {
+ if (aDesc.isAccessorDescriptor()) {
+ return aResult.failNotDataDescriptor();
+ }
+ if (aDesc.hasConfigurable() && !aDesc.configurable()) {
+ return aResult.failInvalidDescriptor();
+ }
+ if (aDesc.hasEnumerable() && !aDesc.enumerable()) {
+ return aResult.failInvalidDescriptor();
+ }
+ if (aDesc.hasWritable() && !aDesc.writable()) {
+ return aResult.failInvalidDescriptor();
+ }
+ if (aDesc.hasValue()) {
+ JS::RootedObject backingListObj(aCx);
+ if (!GetBackingListObject(aCx, aProxy, &backingListObj)) {
+ return false;
+ }
+
+ return SetIndexedValue(aCx, aProxy, backingListObj, index, aDesc.value(),
+ aResult);
+ }
+ return aResult.succeed();
+ }
+
+ return ForwardingProxyHandler::defineProperty(aCx, aProxy, aId, aDesc,
+ aResult);
+}
+
+bool ObservableArrayProxyHandler::delete_(JSContext* aCx,
+ JS::HandleObject aProxy,
+ JS::HandleId aId,
+ JS::ObjectOpResult& aResult) const {
+ if (aId.get() == s_length_id) {
+ return aResult.failCantDelete();
+ }
+ uint32_t index = GetArrayIndexFromId(aId);
+ if (IsArrayIndex(index)) {
+ JS::RootedObject backingListObj(aCx);
+ if (!GetBackingListObject(aCx, aProxy, &backingListObj)) {
+ return false;
+ }
+
+ uint32_t oldLen = 0;
+ if (!JS::GetArrayLength(aCx, backingListObj, &oldLen)) {
+ return false;
+ }
+
+ // We do not follow the spec (step 3.3 in
+ // https://webidl.spec.whatwg.org/#es-observable-array-deleteProperty)
+ // is because `oldLen - 1` could be `-1` if the backing list is empty, but
+ // `oldLen` is `uint32_t` in practice. See also
+ // https://github.com/whatwg/webidl/issues/1049.
+ if (oldLen != index + 1) {
+ return aResult.failBadIndex();
+ }
+
+ JS::RootedValue value(aCx);
+ if (!JS_GetElement(aCx, backingListObj, index, &value)) {
+ return false;
+ }
+
+ if (!OnDeleteItem(aCx, aProxy, value, index)) {
+ return false;
+ }
+
+ if (!JS::SetArrayLength(aCx, backingListObj, index)) {
+ return false;
+ }
+
+ return aResult.succeed();
+ }
+ return ForwardingProxyHandler::delete_(aCx, aProxy, aId, aResult);
+}
+
+bool ObservableArrayProxyHandler::get(JSContext* aCx, JS::HandleObject aProxy,
+ JS::HandleValue aReceiver,
+ JS::HandleId aId,
+ JS::MutableHandleValue aVp) const {
+ JS::RootedObject backingListObj(aCx);
+ if (!GetBackingListObject(aCx, aProxy, &backingListObj)) {
+ return false;
+ }
+
+ uint32_t length = 0;
+ if (!JS::GetArrayLength(aCx, backingListObj, &length)) {
+ return false;
+ }
+
+ if (aId.get() == s_length_id) {
+ return ToJSValue(aCx, length, aVp);
+ }
+ uint32_t index = GetArrayIndexFromId(aId);
+ if (IsArrayIndex(index)) {
+ if (index >= length) {
+ aVp.setUndefined();
+ return true;
+ }
+ return JS_GetElement(aCx, backingListObj, index, aVp);
+ }
+ return ForwardingProxyHandler::get(aCx, aProxy, aReceiver, aId, aVp);
+}
+
+bool ObservableArrayProxyHandler::getOwnPropertyDescriptor(
+ JSContext* aCx, JS::HandleObject aProxy, JS::HandleId aId,
+ JS::MutableHandle> aDesc) const {
+ JS::RootedObject backingListObj(aCx);
+ if (!GetBackingListObject(aCx, aProxy, &backingListObj)) {
+ return false;
+ }
+
+ uint32_t length = 0;
+ if (!JS::GetArrayLength(aCx, backingListObj, &length)) {
+ return false;
+ }
+
+ if (aId.get() == s_length_id) {
+ JS::RootedValue value(aCx, JS::NumberValue(length));
+ aDesc.set(Some(JS::PropertyDescriptor::Data(
+ value, {JS::PropertyAttribute::Writable})));
+ return true;
+ }
+ uint32_t index = GetArrayIndexFromId(aId);
+ if (IsArrayIndex(index)) {
+ if (index >= length) {
+ return true;
+ }
+
+ JS::RootedValue value(aCx);
+ if (!JS_GetElement(aCx, backingListObj, index, &value)) {
+ return false;
+ }
+
+ aDesc.set(Some(JS::PropertyDescriptor::Data(
+ value,
+ {JS::PropertyAttribute::Configurable, JS::PropertyAttribute::Writable,
+ JS::PropertyAttribute::Enumerable})));
+ return true;
+ }
+ return ForwardingProxyHandler::getOwnPropertyDescriptor(aCx, aProxy, aId,
+ aDesc);
+}
+
+bool ObservableArrayProxyHandler::has(JSContext* aCx, JS::HandleObject aProxy,
+ JS::HandleId aId, bool* aBp) const {
+ if (aId.get() == s_length_id) {
+ *aBp = true;
+ return true;
+ }
+ uint32_t index = GetArrayIndexFromId(aId);
+ if (IsArrayIndex(index)) {
+ uint32_t length = 0;
+ if (!GetBackingListLength(aCx, aProxy, &length)) {
+ return false;
+ }
+
+ *aBp = (index < length);
+ return true;
+ }
+ return ForwardingProxyHandler::has(aCx, aProxy, aId, aBp);
+}
+
+bool ObservableArrayProxyHandler::ownPropertyKeys(
+ JSContext* aCx, JS::HandleObject aProxy,
+ JS::MutableHandleVector aProps) const {
+ uint32_t length = 0;
+ if (!GetBackingListLength(aCx, aProxy, &length)) {
+ return false;
+ }
+
+ for (int32_t i = 0; i < int32_t(length); i++) {
+ if (!aProps.append(JS::PropertyKey::Int(i))) {
+ return false;
+ }
+ }
+ return ForwardingProxyHandler::ownPropertyKeys(aCx, aProxy, aProps);
+}
+
+bool ObservableArrayProxyHandler::preventExtensions(
+ JSContext* aCx, JS::HandleObject aProxy,
+ JS::ObjectOpResult& aResult) const {
+ return aResult.failCantPreventExtensions();
+}
+
+bool ObservableArrayProxyHandler::set(JSContext* aCx, JS::HandleObject aProxy,
+ JS::HandleId aId, JS::HandleValue aV,
+ JS::HandleValue aReceiver,
+ JS::ObjectOpResult& aResult) const {
+ if (aId.get() == s_length_id) {
+ JS::RootedObject backingListObj(aCx);
+ if (!GetBackingListObject(aCx, aProxy, &backingListObj)) {
+ return false;
+ }
+
+ return SetLength(aCx, aProxy, backingListObj, aV, aResult);
+ }
+ uint32_t index = GetArrayIndexFromId(aId);
+ if (IsArrayIndex(index)) {
+ JS::RootedObject backingListObj(aCx);
+ if (!GetBackingListObject(aCx, aProxy, &backingListObj)) {
+ return false;
+ }
+
+ return SetIndexedValue(aCx, aProxy, backingListObj, index, aV, aResult);
+ }
+ return ForwardingProxyHandler::set(aCx, aProxy, aId, aV, aReceiver, aResult);
+}
+
+bool ObservableArrayProxyHandler::GetBackingListObject(
+ JSContext* aCx, JS::HandleObject aProxy,
+ JS::MutableHandleObject aBackingListObject) const {
+ // Retrieve the backing list object from the reserved slot on the proxy
+ // object. If it doesn't exist yet, create it.
+ JS::RootedValue slotValue(aCx);
+ slotValue = js::GetProxyReservedSlot(
+ aProxy, OBSERVABLE_ARRAY_BACKING_LIST_OBJECT_SLOT);
+ if (slotValue.isUndefined()) {
+ JS::RootedObject newBackingListObj(aCx);
+ newBackingListObj.set(JS::NewArrayObject(aCx, 0));
+ if (NS_WARN_IF(!newBackingListObj)) {
+ return false;
+ }
+ slotValue = JS::ObjectValue(*newBackingListObj);
+ js::SetProxyReservedSlot(aProxy, OBSERVABLE_ARRAY_BACKING_LIST_OBJECT_SLOT,
+ slotValue);
+ }
+ aBackingListObject.set(&slotValue.toObject());
+ return true;
+}
+
+bool ObservableArrayProxyHandler::GetBackingListLength(
+ JSContext* aCx, JS::HandleObject aProxy, uint32_t* aLength) const {
+ JS::RootedObject backingListObj(aCx);
+ if (!GetBackingListObject(aCx, aProxy, &backingListObj)) {
+ return false;
+ }
+
+ return JS::GetArrayLength(aCx, backingListObj, aLength);
+}
+
+bool ObservableArrayProxyHandler::SetLength(JSContext* aCx,
+ JS::HandleObject aProxy,
+ uint32_t aLength) const {
+ JS::RootedObject backingListObj(aCx);
+ if (!GetBackingListObject(aCx, aProxy, &backingListObj)) {
+ return false;
+ }
+
+ JS::ObjectOpResult result;
+ if (!SetLength(aCx, aProxy, backingListObj, aLength, result)) {
+ return false;
+ }
+
+ return result ? true : result.reportError(aCx, aProxy);
+}
+
+bool ObservableArrayProxyHandler::SetLength(JSContext* aCx,
+ JS::HandleObject aProxy,
+ JS::HandleObject aBackingList,
+ uint32_t aLength,
+ JS::ObjectOpResult& aResult) const {
+ uint32_t oldLen;
+ if (!JS::GetArrayLength(aCx, aBackingList, &oldLen)) {
+ return false;
+ }
+
+ if (aLength > oldLen) {
+ return aResult.failBadArrayLength();
+ }
+
+ bool ok = true;
+ uint32_t len = oldLen;
+ for (; len > aLength; len--) {
+ uint32_t indexToDelete = len - 1;
+ JS::RootedValue value(aCx);
+ if (!JS_GetElement(aCx, aBackingList, indexToDelete, &value)) {
+ ok = false;
+ break;
+ }
+
+ if (!OnDeleteItem(aCx, aProxy, value, indexToDelete)) {
+ ok = false;
+ break;
+ }
+ }
+
+ return JS::SetArrayLength(aCx, aBackingList, len) && ok ? aResult.succeed()
+ : false;
+}
+
+bool ObservableArrayProxyHandler::SetLength(JSContext* aCx,
+ JS::HandleObject aProxy,
+ JS::HandleObject aBackingList,
+ JS::HandleValue aValue,
+ JS::ObjectOpResult& aResult) const {
+ uint32_t uint32Len;
+ if (!ToUint32(aCx, aValue, &uint32Len)) {
+ return false;
+ }
+
+ double numberLen;
+ if (!ToNumber(aCx, aValue, &numberLen)) {
+ return false;
+ }
+
+ if (uint32Len != numberLen) {
+ JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr,
+ JSMSG_BAD_INDEX);
+ return false;
+ }
+
+ return SetLength(aCx, aProxy, aBackingList, uint32Len, aResult);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/bindings/ObservableArrayProxyHandler.h b/dom/bindings/ObservableArrayProxyHandler.h
new file mode 100644
index 000000000000..cf31dfc25ae5
--- /dev/null
+++ b/dom/bindings/ObservableArrayProxyHandler.h
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_ObservableArrayProxyHandler_h
+#define mozilla_dom_ObservableArrayProxyHandler_h
+
+#include "js/TypeDecls.h"
+#include "js/Wrapper.h"
+
+namespace mozilla {
+namespace dom {
+
+/**
+ * Proxy handler for observable array exotic object.
+ *
+ * The indexed properties are stored in the backing list object in reserved slot
+ * of the proxy object with special treatment intact.
+ *
+ * The additional properties are stored in the proxy target object.
+ */
+
+class ObservableArrayProxyHandler : public js::ForwardingProxyHandler {
+ public:
+ explicit constexpr ObservableArrayProxyHandler()
+ : js::ForwardingProxyHandler(&family) {}
+
+ // Implementations of methods that can be implemented in terms of
+ // other lower-level methods.
+ bool defineProperty(JSContext* aCx, JS::HandleObject aProxy, JS::HandleId aId,
+ JS::Handle aDesc,
+ JS::ObjectOpResult& aResult) const override;
+
+ bool delete_(JSContext* aCx, JS::HandleObject aProxy, JS::HandleId aId,
+ JS::ObjectOpResult& aResult) const override;
+
+ bool get(JSContext* aCx, JS::HandleObject aProxy, JS::HandleValue aReceiver,
+ JS::HandleId aId, JS::MutableHandleValue aVp) const override;
+
+ bool getOwnPropertyDescriptor(
+ JSContext* aCx, JS::HandleObject aProxy, JS::HandleId aId,
+ JS::MutableHandle> aDesc) const override;
+
+ bool has(JSContext* aCx, JS::HandleObject aProxy, JS::HandleId aId,
+ bool* aBp) const override;
+
+ bool ownPropertyKeys(JSContext* aCx, JS::HandleObject aProxy,
+ JS::MutableHandleVector aProps) const override;
+
+ bool preventExtensions(JSContext* aCx, JS::HandleObject aProxy,
+ JS::ObjectOpResult& aResult) const override;
+
+ bool set(JSContext* aCx, JS::HandleObject aProxy, JS::HandleId aId,
+ JS::HandleValue aV, JS::HandleValue aReceiver,
+ JS::ObjectOpResult& aResult) const override;
+
+ bool SetLength(JSContext* aCx, JS::HandleObject aProxy,
+ uint32_t aLength) const;
+
+ static const char family;
+
+ protected:
+ bool GetBackingListObject(JSContext* aCx, JS::HandleObject aProxy,
+ JS::MutableHandleObject aBackingListObject) const;
+
+ bool GetBackingListLength(JSContext* aCx, JS::HandleObject aProxy,
+ uint32_t* aLength) const;
+
+ bool SetLength(JSContext* aCx, JS::HandleObject aProxy,
+ JS::HandleObject aBackingList, uint32_t aLength,
+ JS::ObjectOpResult& aResult) const;
+
+ bool SetLength(JSContext* aCx, JS::HandleObject aProxy,
+ JS::HandleObject aBackingList, JS::HandleValue aValue,
+ JS::ObjectOpResult& aResult) const;
+
+ // Hook for subclasses to invoke the setting the indexed value steps which
+ // would invoke DeleteAlgorithm/SetAlgorithm defined and implemented per
+ // interface. Returns false and throw exception on failure.
+ virtual bool SetIndexedValue(JSContext* aCx, JS::HandleObject aProxy,
+ JS::HandleObject aBackingList, uint32_t aIndex,
+ JS::HandleValue aValue,
+ JS::ObjectOpResult& aResult) const = 0;
+
+ // Hook for subclasses to invoke the DeleteAlgorithm defined and implemented
+ // per interface. Returns false and throw exception on failure.
+ virtual bool OnDeleteItem(JSContext* aCx, JS::HandleObject aProxy,
+ JS::HandleValue aValue, uint32_t aIndex) const = 0;
+};
+
+inline bool IsObservableArrayProxy(JSObject* obj) {
+ return js::IsProxy(obj) && js::GetProxyHandler(obj)->family() ==
+ &ObservableArrayProxyHandler::family;
+}
+
+inline const ObservableArrayProxyHandler* GetObservableArrayProxyHandler(
+ JSObject* obj) {
+ MOZ_ASSERT(IsObservableArrayProxy(obj));
+ return static_cast(
+ js::GetProxyHandler(obj));
+}
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_ObservableArrayProxyHandler_h */
diff --git a/dom/bindings/ProxyHandlerUtils.h b/dom/bindings/ProxyHandlerUtils.h
new file mode 100644
index 000000000000..0babda280d99
--- /dev/null
+++ b/dom/bindings/ProxyHandlerUtils.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_ProxyHandlerUtils_h
+#define mozilla_dom_ProxyHandlerUtils_h
+
+#include "mozilla/Likely.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TextUtils.h"
+
+#include "js/Id.h"
+#include "js/Object.h" // JS::GetClass
+#include "js/PropertyDescriptor.h"
+#include "js/String.h" // JS::AtomToLinearString, JS::GetLinearString{CharAt,Length}
+#include "js/TypeDecls.h"
+
+#include "jsfriendapi.h" // js::StringIsArrayIndex
+
+namespace mozilla {
+namespace dom {
+
+extern jsid s_length_id;
+
+// A return value of UINT32_MAX indicates "not an array index". Note, in
+// particular, that UINT32_MAX itself is not a valid array index in general.
+inline uint32_t GetArrayIndexFromId(JS::Handle id) {
+ // Much like js::IdIsIndex, except with a fast path for "length" and another
+ // fast path for starting with a lowercase ascii char. Is that second one
+ // really needed? I guess it is because StringIsArrayIndex is out of line...
+ // as of now, use id.get() instead of id otherwise operands mismatch error
+ // occurs.
+ if (MOZ_LIKELY(id.isInt())) {
+ return id.toInt();
+ }
+ if (MOZ_LIKELY(id.get() == s_length_id)) {
+ return UINT32_MAX;
+ }
+ if (MOZ_UNLIKELY(!id.isAtom())) {
+ return UINT32_MAX;
+ }
+
+ JSLinearString* str = JS::AtomToLinearString(id.toAtom());
+ if (MOZ_UNLIKELY(JS::GetLinearStringLength(str) == 0)) {
+ return UINT32_MAX;
+ }
+
+ char16_t firstChar = JS::GetLinearStringCharAt(str, 0);
+ if (MOZ_LIKELY(IsAsciiLowercaseAlpha(firstChar))) {
+ return UINT32_MAX;
+ }
+
+ uint32_t i;
+ return js::StringIsArrayIndex(str, &i) ? i : UINT32_MAX;
+}
+
+inline bool IsArrayIndex(uint32_t index) { return index < UINT32_MAX; }
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_ProxyHandlerUtils_h */
diff --git a/dom/bindings/ToJSValue.h b/dom/bindings/ToJSValue.h
index 42805febdf50..a087c6bb6e1e 100644
--- a/dom/bindings/ToJSValue.h
+++ b/dom/bindings/ToJSValue.h
@@ -275,6 +275,12 @@ template
return ToJSValue(aCx, *aArgument.get(), aValue);
}
+template
+[[nodiscard]] bool ToJSValue(JSContext* aCx, const OwningNonNull& aArgument,
+ JS::MutableHandle aValue) {
+ return ToJSValue(aCx, *aArgument.get(), aValue);
+}
+
// Accept WebIDL dictionaries
template
[[nodiscard]] std::enable_if_t::value, bool>
diff --git a/dom/bindings/WebIDLGlobalNameHash.cpp b/dom/bindings/WebIDLGlobalNameHash.cpp
index 37022dcf462c..4ed6b96706db 100644
--- a/dom/bindings/WebIDLGlobalNameHash.cpp
+++ b/dom/bindings/WebIDLGlobalNameHash.cpp
@@ -18,10 +18,10 @@
#include "mozilla/Maybe.h"
#include "mozilla/dom/BindingNames.h"
#include "mozilla/dom/DOMJSClass.h"
-#include "mozilla/dom/DOMJSProxyHandler.h"
#include "mozilla/dom/Exceptions.h"
#include "mozilla/dom/JSSlots.h"
#include "mozilla/dom/PrototypeList.h"
+#include "mozilla/dom/ProxyHandlerUtils.h"
#include "mozilla/dom/RegisterBindings.h"
#include "nsGlobalWindow.h"
#include "nsTHashtable.h"
diff --git a/dom/bindings/moz.build b/dom/bindings/moz.build
index a5b33b7b721b..92273e06d36f 100644
--- a/dom/bindings/moz.build
+++ b/dom/bindings/moz.build
@@ -43,8 +43,10 @@ EXPORTS.mozilla.dom += [
"JSSlots.h",
"NonRefcountedDOMObject.h",
"Nullable.h",
+ "ObservableArrayProxyHandler.h",
"PinnedStringId.h",
"PrimitiveConversions.h",
+ "ProxyHandlerUtils.h",
"Record.h",
"RemoteObjectProxy.h",
"RootedDictionary.h",
@@ -116,6 +118,7 @@ UNIFIED_SOURCES += [
"IterableIterator.cpp",
"nsScriptError.cpp",
"nsScriptErrorWithStack.cpp",
+ "ObservableArrayProxyHandler.cpp",
"RemoteObjectProxy.cpp",
"SimpleGlobalObject.cpp",
"ToJSValue.cpp",
@@ -136,6 +139,7 @@ if CONFIG["MOZ_DEBUG"] and CONFIG["ENABLE_TESTS"]:
"test/TestInterfaceMaplike.h",
"test/TestInterfaceMaplikeJSObject.h",
"test/TestInterfaceMaplikeObject.h",
+ "test/TestInterfaceObservableArray.h",
"test/TestInterfaceSetlike.h",
"test/TestInterfaceSetlikeNode.h",
"test/TestTrialInterface.h",
@@ -149,6 +153,7 @@ if CONFIG["MOZ_DEBUG"] and CONFIG["ENABLE_TESTS"]:
"test/TestInterfaceMaplike.cpp",
"test/TestInterfaceMaplikeJSObject.cpp",
"test/TestInterfaceMaplikeObject.cpp",
+ "test/TestInterfaceObservableArray.cpp",
"test/TestInterfaceSetlike.cpp",
"test/TestInterfaceSetlikeNode.cpp",
"test/TestTrialInterface.cpp",
diff --git a/dom/bindings/parser/WebIDL.py b/dom/bindings/parser/WebIDL.py
index c1ac3d583553..2c6f108d29cd 100644
--- a/dom/bindings/parser/WebIDL.py
+++ b/dom/bindings/parser/WebIDL.py
@@ -1034,6 +1034,14 @@ class IDLInterfaceOrNamespace(IDLInterfaceOrInterfaceMixinOrNamespace):
% (member.maplikeOrSetlikeOrIterableType),
[member.location],
)
+ if member.valueType.isObservableArray() or (
+ member.hasKeyType() and member.keyType.isObservableArray()
+ ):
+ raise WebIDLError(
+ "%s declaration uses ObservableArray as value or key type"
+ % (member.maplikeOrSetlikeOrIterableType),
+ [member.location],
+ )
# Check that we only have one interface declaration (currently
# there can only be one maplike/setlike declaration per
# interface)
@@ -1299,12 +1307,13 @@ class IDLInterfaceOrNamespace(IDLInterfaceOrInterfaceMixinOrNamespace):
and (
member.getExtendedAttribute("StoreInSlot")
or member.getExtendedAttribute("Cached")
+ or member.type.isObservableArray()
)
) or member.isMaplikeOrSetlike():
if self.isJSImplemented() and not member.isMaplikeOrSetlike():
raise WebIDLError(
"Interface %s is JS-implemented and we "
- "don't support [Cached] or [StoreInSlot] "
+ "don't support [Cached] or [StoreInSlot] or ObservableArray "
"on JS-implemented interfaces" % self.identifier.name,
[self.location, member.location],
)
@@ -2351,6 +2360,7 @@ class IDLType(IDLObject):
"sequence",
"record",
"promise",
+ "observablearray",
)
def __init__(self, location, name):
@@ -2498,6 +2508,9 @@ class IDLType(IDLObject):
def isJSONType(self):
return False
+ def isObservableArray(self):
+ return False
+
def hasClamp(self):
return self._clamp
@@ -2723,6 +2736,9 @@ class IDLNullableType(IDLParametrizedType):
def isJSONType(self):
return self.inner.isJSONType()
+ def isObservableArray(self):
+ return self.inner.isObservableArray()
+
def hasClamp(self):
return self.inner.hasClamp()
@@ -2745,7 +2761,7 @@ class IDLNullableType(IDLParametrizedType):
if self.inner.nullable():
raise WebIDLError(
- "The inner type of a nullable type must not be " "a nullable type",
+ "The inner type of a nullable type must not be a nullable type",
[self.location, self.inner.location],
)
if self.inner.isUnion():
@@ -2762,6 +2778,11 @@ class IDLNullableType(IDLParametrizedType):
"[LegacyNullToEmptyString] not allowed on a nullable DOMString",
[self.location, self.inner.location],
)
+ if self.inner.isObservableArray():
+ raise WebIDLError(
+ "The inner type of a nullable type must not be an ObservableArray type",
+ [self.location, self.inner.location],
+ )
self.name = self.inner.name + "OrNull"
return self
@@ -2821,6 +2842,12 @@ class IDLSequenceType(IDLParametrizedType):
return IDLType.Tags.sequence
def complete(self, scope):
+ if self.inner.isObservableArray():
+ raise WebIDLError(
+ "The inner type of a sequence type must not be an ObservableArray type",
+ [self.location, self.inner.location],
+ )
+
self.inner = self.inner.complete(scope)
self.name = self.inner.name + "Sequence"
return self
@@ -2878,6 +2905,12 @@ class IDLRecordType(IDLParametrizedType):
return IDLType.Tags.record
def complete(self, scope):
+ if self.inner.isObservableArray():
+ raise WebIDLError(
+ "The value type of a record type must not be an ObservableArray type",
+ [self.location, self.inner.location],
+ )
+
self.inner = self.inner.complete(scope)
self.name = self.keyType.name + self.inner.name + "Record"
return self
@@ -2906,6 +2939,75 @@ class IDLRecordType(IDLParametrizedType):
return self.inner.unroll().isExposedInAllOf(exposureSet)
+class IDLObservableArrayType(IDLParametrizedType):
+ def __init__(self, location, innerType):
+ assert not innerType.isVoid()
+ IDLParametrizedType.__init__(self, location, None, innerType)
+
+ def __hash__(self):
+ return hash(self.inner)
+
+ def __eq__(self, other):
+ return isinstance(other, IDLObservableArrayType) and self.inner == other.inner
+
+ def __str__(self):
+ return self.inner.__str__() + "ObservableArray"
+
+ def prettyName(self):
+ return "ObservableArray<%s>" % self.inner.prettyName()
+
+ def isVoid(self):
+ return False
+
+ def isJSONType(self):
+ return self.inner.isJSONType()
+
+ def isObservableArray(self):
+ return True
+
+ def isComplete(self):
+ return self.name is not None
+
+ def tag(self):
+ return IDLType.Tags.observablearray
+
+ def complete(self, scope):
+ if not self.inner.isComplete():
+ self.inner = self.inner.complete(scope)
+ assert self.inner.isComplete()
+
+ if self.inner.isDictionary():
+ raise WebIDLError(
+ "The inner type of an ObservableArray type must not "
+ "be a dictionary type",
+ [self.location, self.inner.location],
+ )
+ if self.inner.isSequence():
+ raise WebIDLError(
+ "The inner type of an ObservableArray type must not "
+ "be a sequence type",
+ [self.location, self.inner.location],
+ )
+ if self.inner.isRecord():
+ raise WebIDLError(
+ "The inner type of an ObservableArray type must not be a record type",
+ [self.location, self.inner.location],
+ )
+ if self.inner.isObservableArray():
+ raise WebIDLError(
+ "The inner type of an ObservableArray type must not "
+ "be an ObservableArray type",
+ [self.location, self.inner.location],
+ )
+
+ self.name = self.inner.name + "ObservableArray"
+ return self
+
+ def isDistinguishableFrom(self, other):
+ # ObservableArrays are not distinguishable from anything.
+ return False
+
+
class IDLUnionType(IDLType):
def __init__(self, location, memberTypes):
IDLType.__init__(self, location, "")
@@ -3388,6 +3490,12 @@ class IDLPromiseType(IDLParametrizedType):
return IDLType.Tags.promise
def complete(self, scope):
+ if self.inner.isObservableArray():
+ raise WebIDLError(
+ "The inner type of a promise type must not be an ObservableArray type",
+ [self.location, self.inner.location],
+ )
+
self.inner = self.promiseInnerType().complete(scope)
return self
@@ -5064,6 +5172,21 @@ class IDLAttribute(IDLInterfaceMember):
"Promise-returning attributes must be readonly", [self.location]
)
+ if self.type.isObservableArray():
+ if self.isStatic():
+ raise WebIDLError(
+ "A static attribute cannot have an ObservableArray type",
+ [self.location],
+ )
+ if self.getExtendedAttribute("Cached") or self.getExtendedAttribute(
+ "StoreInSlot"
+ ):
+ raise WebIDLError(
+ "[Cached] and [StoreInSlot] must not be used "
+ "on an attribute whose type is ObservableArray",
+ [self.location],
+ )
+
def validate(self):
def typeContainsChromeOnlyDictionaryMember(type):
if type.nullable() or type.isSequence() or type.isRecord():
@@ -5608,6 +5731,12 @@ class IDLArgument(IDLObjectWithIdentifier):
"Dictionary members cannot be [LegacyNullToEmptyString]",
[self.location],
)
+ if self.type.isObservableArray():
+ raise WebIDLError(
+ "%s cannot have an ObservableArray type"
+ % ("Dictionary members" if self.dictionaryMember else "Arguments"),
+ [self.location],
+ )
# Now do the coercing thing; this needs to happen after the
# above creation of a default value.
if self.defaultValue:
@@ -6692,6 +6821,7 @@ class Tokenizer(object):
"float": "FLOAT",
"long": "LONG",
"object": "OBJECT",
+ "ObservableArray": "OBSERVABLEARRAY",
"octet": "OCTET",
"Promise": "PROMISE",
"required": "REQUIRED",
@@ -8231,6 +8361,14 @@ class Parser(Tokenizer):
type = IDLRecordType(self.getLocation(p, 1), keyType, valueType)
p[0] = self.handleNullable(type, p[7])
+ def p_DistinguishableTypeObservableArrayType(self, p):
+ """
+ DistinguishableType : OBSERVABLEARRAY LT TypeWithExtendedAttributes GT Null
+ """
+ innerType = p[3]
+ type = IDLObservableArrayType(self.getLocation(p, 1), innerType)
+ p[0] = self.handleNullable(type, p[5])
+
def p_DistinguishableTypeScopedName(self, p):
"""
DistinguishableType : ScopedName Null
diff --git a/dom/bindings/parser/runtests.py b/dom/bindings/parser/runtests.py
index 0204fc91a437..abf16a7b190c 100644
--- a/dom/bindings/parser/runtests.py
+++ b/dom/bindings/parser/runtests.py
@@ -3,7 +3,8 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import print_function
-import os, sys
+import os
+import sys
import glob
import argparse
import traceback
@@ -53,6 +54,17 @@ class TestHarness(object):
else:
self.test_fail(msg + " | Got %s expected %s" % (a, b))
+ def should_throw(self, parser, code, msg):
+ parser = parser.reset()
+ threw = False
+ try:
+ parser.parse(code)
+ parser.finish()
+ except:
+ threw = True
+
+ self.ok(threw, "Should have thrown: %s" % msg)
+
def run_tests(tests, verbose):
testdir = os.path.join(os.path.dirname(__file__), "tests")
diff --git a/dom/bindings/parser/tests/test_observableArray.py b/dom/bindings/parser/tests/test_observableArray.py
new file mode 100644
index 000000000000..3ddd6fe6f9d0
--- /dev/null
+++ b/dom/bindings/parser/tests/test_observableArray.py
@@ -0,0 +1,302 @@
+# 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/.
+
+
+def WebIDLTest(parser, harness):
+
+ # Test void as inner type
+ # `void` could only be used as return type or the parameter of a promise
+ # type. This test should no longer be valid after bug 1659158 that we change
+ # `void` to `undefined`.
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ attribute ObservableArray foo;
+ };
+ """,
+ "use void as inner type",
+ )
+
+ # Test dictionary as inner type
+ harness.should_throw(
+ parser,
+ """
+ dictionary A {
+ boolean member;
+ };
+ interface B {
+ attribute ObservableArray foo;
+ };
+ """,
+ "use dictionary as inner type",
+ )
+
+ # Test sequence as inner type
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ attribute ObservableArray> foo;
+ };
+ """,
+ "use sequence as inner type",
+ )
+
+ # Test sequence as inner type
+ harness.should_throw(
+ parser,
+ """
+ dictionary A {
+ boolean member;
+ };
+ interface B {
+ attribute ObservableArray> foo;
+ };
+ """,
+ "use sequence as inner type",
+ )
+
+ # Test record as inner type
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ attribute ObservableArray> foo;
+ };
+ """,
+ "use record as inner type",
+ )
+
+ # Test record as inner type
+ harness.should_throw(
+ parser,
+ """
+ dictionary A {
+ boolean member;
+ };
+ interface B {
+ attribute ObservableArray> foo;
+ };
+ """,
+ "use record as inner type",
+ )
+
+ # Test observable array as inner type
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ attribute ObservableArray> foo;
+ };
+ """,
+ "use ObservableArray as inner type",
+ )
+
+ # Test nullable attribute
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ attribute ObservableArray? foo;
+ };
+ """,
+ "nullable",
+ )
+
+ # Test sequence
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ void foo(sequence> foo);
+ };
+ """,
+ "used in sequence",
+ )
+
+ # Test record
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ void foo(record> foo);
+ };
+ """,
+ "used in record",
+ )
+
+ # Test promise
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ Promise> foo();
+ };
+ """,
+ "used in promise",
+ )
+
+ # Test union
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ attribute (DOMString or ObservableArray>) foo;
+ };
+ """,
+ "used in union",
+ )
+
+ # Test dictionary member
+ harness.should_throw(
+ parser,
+ """
+ dictionary A {
+ ObservableArray foo;
+ };
+ """,
+ "used on dictionary member type",
+ )
+
+ # Test argument
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ void foo(ObservableArray foo);
+ };
+ """,
+ "used on argument",
+ )
+
+ # Test static attribute
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ static attribute ObservableArray foo;
+ };
+ """,
+ "used on static attribute type",
+ )
+
+ # Test iterable
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ iterable>;
+ };
+ """,
+ "used in iterable",
+ )
+
+ # Test maplike
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ maplike>;
+ };
+ """,
+ "used in maplike",
+ )
+
+ # Test setlike
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ setlike>;
+ };
+ """,
+ "used in setlike",
+ )
+
+ # Test JS implemented interface
+ harness.should_throw(
+ parser,
+ """
+ [JSImplementation="@mozilla.org/dom/test-interface-js;1"]
+ interface A {
+ readonly attribute ObservableArray foo;
+ };
+ """,
+ "used in JS implemented interface",
+ )
+
+ # Test namespace
+ harness.should_throw(
+ parser,
+ """
+ namespace A {
+ readonly attribute ObservableArray foo;
+ };
+ """,
+ "used in namespaces",
+ )
+
+ # Test [Cached] extended attribute
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ [Cached, Pure]
+ readonly attribute ObservableArray foo;
+ };
+ """,
+ "have Cached extended attribute",
+ )
+
+ # Test [StoreInSlot] extended attribute
+ harness.should_throw(
+ parser,
+ """
+ interface A {
+ [StoreInSlot, Pure]
+ readonly attribute ObservableArray foo;
+ };
+ """,
+ "have StoreInSlot extended attribute",
+ )
+
+ # Test regular attribute
+ parser = parser.reset()
+ parser.parse(
+ """
+ interface A {
+ readonly attribute ObservableArray foo;
+ attribute ObservableArray<[Clamp] octet> bar;
+ attribute ObservableArray baz;
+ attribute ObservableArray<(boolean or long)> qux;
+ };
+ """
+ )
+ results = parser.finish()
+ A = results[0]
+ foo = A.members[0]
+ harness.ok(foo.readonly, "A.foo is readonly attribute")
+ harness.ok(foo.type.isObservableArray(), "A.foo is ObservableArray type")
+ harness.check(
+ foo.slotIndices[A.identifier.name], 0, "A.foo should be stored in slot"
+ )
+ bar = A.members[1]
+ harness.ok(bar.type.isObservableArray(), "A.bar is ObservableArray type")
+ harness.check(
+ bar.slotIndices[A.identifier.name], 1, "A.bar should be stored in slot"
+ )
+ harness.ok(bar.type.inner.hasClamp(), "A.bar's inner type should be clamped")
+ baz = A.members[2]
+ harness.ok(baz.type.isObservableArray(), "A.baz is ObservableArray type")
+ harness.check(
+ baz.slotIndices[A.identifier.name], 2, "A.baz should be stored in slot"
+ )
+ harness.ok(baz.type.inner.nullable(), "A.baz's inner type should be nullable")
+ qux = A.members[3]
+ harness.ok(qux.type.isObservableArray(), "A.qux is ObservableArray type")
+ harness.check(
+ qux.slotIndices[A.identifier.name], 3, "A.qux should be stored in slot"
+ )
+ harness.ok(qux.type.inner.isUnion(), "A.qux's inner type should be union")
diff --git a/dom/bindings/test/TestBindingHeader.h b/dom/bindings/test/TestBindingHeader.h
index 10ec474880a9..b1d9f7281e36 100644
--- a/dom/bindings/test/TestBindingHeader.h
+++ b/dom/bindings/test/TestBindingHeader.h
@@ -848,6 +848,24 @@ class TestInterface : public nsISupports, public nsWrapperCache {
Promise* ReceivePromise();
already_AddRefed ReceiveAddrefedPromise();
+ // ObservableArray types
+ void OnDeleteBooleanObservableArray(bool, uint32_t, ErrorResult&);
+ void OnSetBooleanObservableArray(bool, uint32_t, ErrorResult&);
+ void OnDeleteObjectObservableArray(JSContext*, JS::Handle,
+ uint32_t, ErrorResult&);
+ void OnSetObjectObservableArray(JSContext*, JS::Handle, uint32_t,
+ ErrorResult&);
+ void OnDeleteAnyObservableArray(JSContext*, JS::Handle, uint32_t,
+ ErrorResult&);
+ void OnSetAnyObservableArray(JSContext*, JS::Handle, uint32_t,
+ ErrorResult&);
+ void OnDeleteInterfaceObservableArray(TestInterface*, uint32_t, ErrorResult&);
+ void OnSetInterfaceObservableArray(TestInterface*, uint32_t, ErrorResult&);
+ void OnDeleteNullableObservableArray(const Nullable&, uint32_t,
+ ErrorResult&);
+ void OnSetNullableObservableArray(const Nullable&, uint32_t,
+ ErrorResult&);
+
// binaryNames tests
void MethodRenamedTo();
void MethodRenamedTo(int8_t);
diff --git a/dom/bindings/test/TestCodeGen.webidl b/dom/bindings/test/TestCodeGen.webidl
index bc12ddd6d505..30841d1b7900 100644
--- a/dom/bindings/test/TestCodeGen.webidl
+++ b/dom/bindings/test/TestCodeGen.webidl
@@ -790,6 +790,13 @@ interface TestInterface {
Promise receivePromise();
Promise receiveAddrefedPromise();
+ // ObservableArray types
+ attribute ObservableArray booleanObservableArray;
+ attribute ObservableArray