summaryrefslogtreecommitdiffstats
path: root/dom/worklet
diff options
context:
space:
mode:
Diffstat (limited to 'dom/worklet')
-rw-r--r--dom/worklet/Worklet.cpp122
-rw-r--r--dom/worklet/Worklet.h81
-rw-r--r--dom/worklet/WorkletFetchHandler.cpp642
-rw-r--r--dom/worklet/WorkletFetchHandler.h120
-rw-r--r--dom/worklet/WorkletGlobalScope.cpp116
-rw-r--r--dom/worklet/WorkletGlobalScope.h102
-rw-r--r--dom/worklet/WorkletImpl.cpp152
-rw-r--r--dom/worklet/WorkletImpl.h131
-rw-r--r--dom/worklet/WorkletThread.cpp474
-rw-r--r--dom/worklet/WorkletThread.h78
-rw-r--r--dom/worklet/loader/WorkletModuleLoader.cpp297
-rw-r--r--dom/worklet/loader/WorkletModuleLoader.h119
-rw-r--r--dom/worklet/loader/moz.build20
-rw-r--r--dom/worklet/moz.build36
-rw-r--r--dom/worklet/tests/common.js23
-rw-r--r--dom/worklet/tests/dynamic_import.js7
-rw-r--r--dom/worklet/tests/invalid_specifier.mjs3
-rw-r--r--dom/worklet/tests/mochitest.ini40
-rw-r--r--dom/worklet/tests/server_import_with_cache.sjs12
-rw-r--r--dom/worklet/tests/specifier_with_user.mjs3
-rw-r--r--dom/worklet/tests/test_audioWorklet.html53
-rw-r--r--dom/worklet/tests/test_audioWorkletGlobalScopeRegisterProcessor.html78
-rw-r--r--dom/worklet/tests/test_audioWorklet_WASM.html85
-rw-r--r--dom/worklet/tests/test_audioWorklet_insecureContext.html29
-rw-r--r--dom/worklet/tests/test_audioWorklet_options.html81
-rw-r--r--dom/worklet/tests/test_basic.html66
-rw-r--r--dom/worklet/tests/test_console.html54
-rw-r--r--dom/worklet/tests/test_dump.html30
-rw-r--r--dom/worklet/tests/test_dynamic_import.html54
-rw-r--r--dom/worklet/tests/test_exception.html73
-rw-r--r--dom/worklet/tests/test_fetch_failed.html39
-rw-r--r--dom/worklet/tests/test_import_with_cache.html49
-rw-r--r--dom/worklet/tests/test_paintWorklet.html49
-rw-r--r--dom/worklet/tests/test_promise.html57
-rw-r--r--dom/worklet/tests/worklet_audioWorklet.js16
-rw-r--r--dom/worklet/tests/worklet_audioWorklet_WASM.js16
-rw-r--r--dom/worklet/tests/worklet_audioWorklet_options.js12
-rw-r--r--dom/worklet/tests/worklet_console.js1
-rw-r--r--dom/worklet/tests/worklet_dump.js1
-rw-r--r--dom/worklet/tests/worklet_exception.js1
-rw-r--r--dom/worklet/tests/worklet_paintWorklet.js5
-rw-r--r--dom/worklet/tests/worklet_promise.js22
-rw-r--r--dom/worklet/tests/worklet_test_audioWorkletGlobalScopeRegisterProcessor.js384
43 files changed, 3833 insertions, 0 deletions
diff --git a/dom/worklet/Worklet.cpp b/dom/worklet/Worklet.cpp
new file mode 100644
index 0000000000..7c37bdbba9
--- /dev/null
+++ b/dom/worklet/Worklet.cpp
@@ -0,0 +1,122 @@
+/* -*- 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 "Worklet.h"
+#include "WorkletThread.h"
+
+#include "mozilla/dom/WorkletFetchHandler.h"
+#include "mozilla/dom/WorkletImpl.h"
+#include "xpcprivate.h"
+
+using JS::loader::ResolveError;
+using JS::loader::ResolveErrorInfo;
+
+namespace mozilla::dom {
+// ---------------------------------------------------------------------------
+// Worklet
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Worklet)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Worklet)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwnedObject)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mImportHandlers)
+ tmp->mImpl->NotifyWorkletFinished();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Worklet)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwnedObject)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mImportHandlers)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(Worklet)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(Worklet)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Worklet)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+Worklet::Worklet(nsPIDOMWindowInner* aWindow, RefPtr<WorkletImpl> aImpl,
+ nsISupports* aOwnedObject)
+ : mWindow(aWindow), mOwnedObject(aOwnedObject), mImpl(std::move(aImpl)) {
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(mImpl);
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+Worklet::~Worklet() { mImpl->NotifyWorkletFinished(); }
+
+JSObject* Worklet::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return mImpl->WrapWorklet(aCx, this, aGivenProto);
+}
+
+static bool LoadLocalizedStrings(nsTArray<nsString>& aStrings) {
+ // All enumes in ResolveError.
+ ResolveError errors[] = {ResolveError::Failure,
+ ResolveError::FailureMayBeBare,
+ ResolveError::BlockedByNullEntry,
+ ResolveError::BlockedByAfterPrefix,
+ ResolveError::BlockedByBacktrackingPrefix,
+ ResolveError::InvalidBareSpecifier};
+
+ static_assert(
+ ArrayLength(errors) == static_cast<size_t>(ResolveError::Length),
+ "The array 'errors' has missing entries in the enum class ResolveError.");
+
+ for (auto i : errors) {
+ nsAutoString message;
+ nsresult rv = nsContentUtils::GetLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, ResolveErrorInfo::GetString(i),
+ message);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ if (NS_WARN_IF(!aStrings.AppendElement(EmptyString(), fallible))) {
+ return false;
+ }
+ } else {
+ if (NS_WARN_IF(!aStrings.AppendElement(message, fallible))) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+already_AddRefed<Promise> Worklet::AddModule(JSContext* aCx,
+ const nsAString& aModuleURL,
+ const WorkletOptions& aOptions,
+ CallerType aCallerType,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mLocalizedStrings.IsEmpty()) {
+ bool result = LoadLocalizedStrings(mLocalizedStrings);
+ if (!result) {
+ return nullptr;
+ }
+ }
+
+ return WorkletFetchHandler::AddModule(this, aCx, aModuleURL, aOptions, aRv);
+}
+
+WorkletFetchHandler* Worklet::GetImportFetchHandler(const nsACString& aURI) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mImportHandlers.GetWeak(aURI);
+}
+
+void Worklet::AddImportFetchHandler(const nsACString& aURI,
+ WorkletFetchHandler* aHandler) {
+ MOZ_ASSERT(aHandler);
+ MOZ_ASSERT(!mImportHandlers.GetWeak(aURI));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mImportHandlers.InsertOrUpdate(aURI, RefPtr{aHandler});
+}
+
+} // namespace mozilla::dom
diff --git a/dom/worklet/Worklet.h b/dom/worklet/Worklet.h
new file mode 100644
index 0000000000..1c404da18d
--- /dev/null
+++ b/dom/worklet/Worklet.h
@@ -0,0 +1,81 @@
+/* -*- 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_Worklet_h
+#define mozilla_dom_Worklet_h
+
+#include "mozilla/Attributes.h"
+#include "nsRefPtrHashtable.h"
+#include "nsWrapperCache.h"
+#include "nsCOMPtr.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+
+class ErrorResult;
+class WorkletImpl;
+
+namespace dom {
+
+class Promise;
+class WorkletFetchHandler;
+class WorkletScriptHandler;
+struct WorkletOptions;
+enum class CallerType : uint32_t;
+
+class Worklet final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Worklet)
+
+ // |aOwnedObject| may be provided by the WorkletImpl as a parent thread
+ // object to keep alive and traverse for CC as long as the Worklet has
+ // references remaining.
+ Worklet(nsPIDOMWindowInner* aWindow, RefPtr<WorkletImpl> aImpl,
+ nsISupports* aOwnedObject = nullptr);
+
+ nsPIDOMWindowInner* GetParentObject() const { return mWindow; }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ already_AddRefed<Promise> AddModule(JSContext* aCx,
+ const nsAString& aModuleURL,
+ const WorkletOptions& aOptions,
+ CallerType aCallerType, ErrorResult& aRv);
+
+ WorkletImpl* Impl() const { return mImpl; }
+
+ const nsTArray<nsString>& GetLocalizedStrings() const {
+ return mLocalizedStrings;
+ }
+
+ private:
+ ~Worklet();
+
+ WorkletFetchHandler* GetImportFetchHandler(const nsACString& aURI);
+
+ void AddImportFetchHandler(const nsACString& aURI,
+ WorkletFetchHandler* aHandler);
+
+ nsCOMPtr<nsPIDOMWindowInner> mWindow;
+ nsCOMPtr<nsISupports> mOwnedObject;
+
+ nsRefPtrHashtable<nsCStringHashKey, WorkletFetchHandler> mImportHandlers;
+
+ const RefPtr<WorkletImpl> mImpl;
+
+ nsTArray<nsString> mLocalizedStrings;
+
+ friend class WorkletFetchHandler;
+ friend class WorkletScriptHandler;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_Worklet_h
diff --git a/dom/worklet/WorkletFetchHandler.cpp b/dom/worklet/WorkletFetchHandler.cpp
new file mode 100644
index 0000000000..bc764c5316
--- /dev/null
+++ b/dom/worklet/WorkletFetchHandler.cpp
@@ -0,0 +1,642 @@
+/* -*- 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 "WorkletFetchHandler.h"
+
+#include "js/loader/ModuleLoadRequest.h"
+#include "js/ContextOptions.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Fetch.h"
+#include "mozilla/dom/Request.h"
+#include "mozilla/dom/Response.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/ScriptLoader.h"
+#include "mozilla/dom/ScriptLoadHandler.h" // ScriptDecoder
+#include "mozilla/dom/Worklet.h"
+#include "mozilla/dom/WorkletBinding.h"
+#include "mozilla/dom/WorkletGlobalScope.h"
+#include "mozilla/dom/WorkletImpl.h"
+#include "mozilla/dom/WorkletThread.h"
+#include "mozilla/dom/worklet/WorkletModuleLoader.h"
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/TaskQueue.h"
+#include "nsIInputStreamPump.h"
+#include "nsIThreadRetargetableRequest.h"
+#include "xpcpublic.h"
+
+using JS::loader::ModuleLoadRequest;
+using JS::loader::ScriptFetchOptions;
+using mozilla::dom::loader::WorkletModuleLoader;
+
+namespace mozilla::dom {
+
+// A Runnable to call ModuleLoadRequest::StartModuleLoad on a worklet thread.
+class StartModuleLoadRunnable final : public Runnable {
+ public:
+ StartModuleLoadRunnable(
+ WorkletImpl* aWorkletImpl,
+ const nsMainThreadPtrHandle<WorkletFetchHandler>& aHandlerRef,
+ nsCOMPtr<nsIURI> aURI, nsIURI* aReferrer,
+ const nsTArray<nsString>& aLocalizedStrs)
+ : Runnable("Worklet::StartModuleLoadRunnable"),
+ mWorkletImpl(aWorkletImpl),
+ mHandlerRef(aHandlerRef),
+ mURI(std::move(aURI)),
+ mReferrer(aReferrer),
+ mLocalizedStrs(aLocalizedStrs),
+ mParentRuntime(
+ JS_GetParentRuntime(CycleCollectedJSContext::Get()->Context())) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mParentRuntime);
+ xpc::SetPrefableContextOptions(mContextOptions);
+ }
+
+ ~StartModuleLoadRunnable() = default;
+
+ NS_IMETHOD Run() override;
+
+ private:
+ NS_IMETHOD RunOnWorkletThread();
+
+ RefPtr<WorkletImpl> mWorkletImpl;
+ nsMainThreadPtrHandle<WorkletFetchHandler> mHandlerRef;
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsIURI> mReferrer;
+ const nsTArray<nsString>& mLocalizedStrs;
+ JSRuntime* mParentRuntime;
+ JS::ContextOptions mContextOptions;
+};
+
+NS_IMETHODIMP
+StartModuleLoadRunnable::Run() {
+ // WorkletThread::IsOnWorkletThread() cannot be used here because it depends
+ // on a WorkletJSContext having been created for this thread. That does not
+ // happen until the global scope is created the first time
+ // RunOnWorkletThread() is called.
+ MOZ_ASSERT(!NS_IsMainThread());
+ return RunOnWorkletThread();
+}
+
+NS_IMETHODIMP StartModuleLoadRunnable::RunOnWorkletThread() {
+ // This can be called on a GraphRunner thread or a DOM Worklet thread.
+ WorkletThread::EnsureCycleCollectedJSContext(mParentRuntime, mContextOptions);
+
+ WorkletGlobalScope* globalScope = mWorkletImpl->GetGlobalScope();
+ if (!globalScope) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ // To fetch a worklet/module worker script graph:
+ // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-worklet/module-worker-script-graph
+ // Step 1. Let options be a script fetch options. And referrer policy is the
+ // empty string.
+ ReferrerPolicy referrerPolicy = ReferrerPolicy::_empty;
+ RefPtr<ScriptFetchOptions> fetchOptions = new ScriptFetchOptions(
+ CORSMode::CORS_NONE, referrerPolicy, /*triggeringPrincipal*/ nullptr);
+
+ WorkletModuleLoader* moduleLoader =
+ static_cast<WorkletModuleLoader*>(globalScope->GetModuleLoader());
+ MOZ_ASSERT(moduleLoader);
+
+ if (!moduleLoader->HasSetLocalizedStrings()) {
+ moduleLoader->SetLocalizedStrings(&mLocalizedStrs);
+ }
+
+ RefPtr<WorkletLoadContext> loadContext = new WorkletLoadContext(mHandlerRef);
+
+ // Part of Step 2. This sets the Top-level flag to true
+ RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest(
+ mURI, fetchOptions, SRIMetadata(), mReferrer, loadContext,
+ true, /* is top level */
+ false, /* is dynamic import */
+ moduleLoader, ModuleLoadRequest::NewVisitedSetForTopLevelImport(mURI),
+ nullptr);
+
+ request->mURL = request->mURI->GetSpecOrDefault();
+
+ return request->StartModuleLoad();
+}
+
+StartFetchRunnable::StartFetchRunnable(
+ const nsMainThreadPtrHandle<WorkletFetchHandler>& aHandlerRef, nsIURI* aURI,
+ nsIURI* aReferrer)
+ : Runnable("Worklet::StartFetchRunnable"),
+ mHandlerRef(aHandlerRef),
+ mURI(aURI),
+ mReferrer(aReferrer) {
+ MOZ_ASSERT(!NS_IsMainThread());
+}
+
+NS_IMETHODIMP
+StartFetchRunnable::Run() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIGlobalObject> global =
+ do_QueryInterface(mHandlerRef->mWorklet->GetParentObject());
+ MOZ_ASSERT(global);
+
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(global))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JSContext* cx = jsapi.cx();
+ nsresult rv = mHandlerRef->StartFetch(cx, mURI, mReferrer);
+ if (NS_FAILED(rv)) {
+ mHandlerRef->HandleFetchFailed(mURI);
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+// A Runnable to call ModuleLoadRequest::OnFetchComplete on a worklet thread.
+class FetchCompleteRunnable final : public Runnable {
+ public:
+ FetchCompleteRunnable(WorkletImpl* aWorkletImpl, nsIURI* aURI,
+ nsresult aResult,
+ UniquePtr<uint8_t[]> aScriptBuffer = nullptr,
+ size_t aScriptLength = 0)
+ : Runnable("Worklet::FetchCompleteRunnable"),
+ mWorkletImpl(aWorkletImpl),
+ mURI(aURI),
+ mResult(aResult),
+ mScriptBuffer(std::move(aScriptBuffer)),
+ mScriptLength(aScriptLength) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ ~FetchCompleteRunnable() = default;
+
+ NS_IMETHOD Run() override;
+
+ private:
+ NS_IMETHOD RunOnWorkletThread();
+
+ RefPtr<WorkletImpl> mWorkletImpl;
+ nsCOMPtr<nsIURI> mURI;
+ nsresult mResult;
+ UniquePtr<uint8_t[]> mScriptBuffer;
+ size_t mScriptLength;
+};
+
+NS_IMETHODIMP
+FetchCompleteRunnable::Run() {
+ MOZ_ASSERT(WorkletThread::IsOnWorkletThread());
+ return RunOnWorkletThread();
+}
+
+NS_IMETHODIMP FetchCompleteRunnable::RunOnWorkletThread() {
+ WorkletGlobalScope* globalScope = mWorkletImpl->GetGlobalScope();
+ if (!globalScope) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ WorkletModuleLoader* moduleLoader =
+ static_cast<WorkletModuleLoader*>(globalScope->GetModuleLoader());
+ MOZ_ASSERT(moduleLoader);
+ MOZ_ASSERT(mURI);
+ ModuleLoadRequest* request = moduleLoader->GetRequest(mURI);
+ MOZ_ASSERT(request);
+
+ // Set the Source type to "text" for decoding.
+ request->SetTextSource();
+
+ nsresult rv;
+ if (mScriptBuffer) {
+ UniquePtr<ScriptDecoder> decoder = MakeUnique<ScriptDecoder>(
+ UTF_8_ENCODING, ScriptDecoder::BOMHandling::Remove);
+ rv = decoder->DecodeRawData(request, mScriptBuffer.get(), mScriptLength,
+ true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ request->mBaseURL = mURI;
+ request->OnFetchComplete(mResult);
+
+ if (NS_FAILED(mResult)) {
+ if (request->IsTopLevel()) {
+ request->LoadFailed();
+ } else {
+ request->Cancel();
+ }
+ }
+
+ moduleLoader->RemoveRequest(mURI);
+ return NS_OK;
+}
+
+//////////////////////////////////////////////////////////////
+// WorkletFetchHandler
+//////////////////////////////////////////////////////////////
+NS_IMPL_CYCLE_COLLECTING_ADDREF(WorkletFetchHandler)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(WorkletFetchHandler)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkletFetchHandler)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(WorkletFetchHandler)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WorkletFetchHandler)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mWorklet, mPromises)
+ tmp->mErrorToRethrow.setUndefined();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WorkletFetchHandler)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWorklet, mPromises)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(WorkletFetchHandler)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mErrorToRethrow)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+// static
+already_AddRefed<Promise> WorkletFetchHandler::AddModule(
+ Worklet* aWorklet, JSContext* aCx, const nsAString& aModuleURL,
+ const WorkletOptions& aOptions, ErrorResult& aRv) {
+ MOZ_ASSERT(aWorklet);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ aWorklet->Impl()->OnAddModuleStarted();
+
+ auto promiseSettledGuard =
+ MakeScopeExit([&] { aWorklet->Impl()->OnAddModulePromiseSettled(); });
+
+ nsCOMPtr<nsIGlobalObject> global =
+ do_QueryInterface(aWorklet->GetParentObject());
+ MOZ_ASSERT(global);
+
+ RefPtr<Promise> promise = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> window = aWorklet->GetParentObject();
+ MOZ_ASSERT(window);
+
+ nsCOMPtr<Document> doc;
+ doc = window->GetExtantDoc();
+ if (!doc) {
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ return promise.forget();
+ }
+
+ nsCOMPtr<nsIURI> resolvedURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(resolvedURI), aModuleURL, nullptr,
+ doc->GetBaseURI());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // https://html.spec.whatwg.org/multipage/worklets.html#dom-worklet-addmodule
+ // Step 3. If this fails, then return a promise rejected with a
+ // "SyntaxError" DOMException.
+ rv = NS_ERROR_DOM_SYNTAX_ERR;
+
+ promise->MaybeReject(rv);
+ return promise.forget();
+ }
+
+ nsAutoCString spec;
+ rv = resolvedURI->GetSpec(spec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ rv = NS_ERROR_DOM_SYNTAX_ERR;
+
+ promise->MaybeReject(rv);
+ return promise.forget();
+ }
+
+ // Maybe we already have an handler for this URI
+ {
+ WorkletFetchHandler* handler = aWorklet->GetImportFetchHandler(spec);
+ if (handler) {
+ handler->AddPromise(aCx, promise);
+ return promise.forget();
+ }
+ }
+
+ RefPtr<WorkletFetchHandler> handler =
+ new WorkletFetchHandler(aWorklet, promise, aOptions.mCredentials);
+
+ nsMainThreadPtrHandle<WorkletFetchHandler> handlerRef{
+ new nsMainThreadPtrHolder<WorkletFetchHandler>("FetchHandler", handler)};
+
+ // Determine request's Referrer
+ // https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
+ // Step 3. Switch on request’s referrer:
+ // "client"
+ // Step 1.4. Let referrerSource be document’s URL.
+ nsIURI* referrer = doc->GetDocumentURIAsReferrer();
+ nsCOMPtr<nsIRunnable> runnable = new StartModuleLoadRunnable(
+ aWorklet->mImpl, handlerRef, std::move(resolvedURI), referrer,
+ aWorklet->GetLocalizedStrings());
+
+ if (NS_FAILED(aWorklet->mImpl->SendControlMessage(runnable.forget()))) {
+ return nullptr;
+ }
+
+ promiseSettledGuard.release();
+
+ aWorklet->AddImportFetchHandler(spec, handler);
+ return promise.forget();
+}
+
+WorkletFetchHandler::WorkletFetchHandler(Worklet* aWorklet, Promise* aPromise,
+ RequestCredentials aCredentials)
+ : mWorklet(aWorklet), mStatus(ePending), mCredentials(aCredentials) {
+ MOZ_ASSERT(aWorklet);
+ MOZ_ASSERT(aPromise);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mPromises.AppendElement(aPromise);
+}
+
+WorkletFetchHandler::~WorkletFetchHandler() { mozilla::DropJSObjects(this); }
+
+void WorkletFetchHandler::ExecutionFailed() {
+ MOZ_ASSERT(NS_IsMainThread());
+ RejectPromises(NS_ERROR_DOM_ABORT_ERR);
+}
+
+void WorkletFetchHandler::ExecutionFailed(JS::Handle<JS::Value> aError) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RejectPromises(aError);
+}
+
+void WorkletFetchHandler::ExecutionSucceeded() {
+ MOZ_ASSERT(NS_IsMainThread());
+ ResolvePromises();
+}
+
+void WorkletFetchHandler::AddPromise(JSContext* aCx, Promise* aPromise) {
+ MOZ_ASSERT(aPromise);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ switch (mStatus) {
+ case ePending:
+ mPromises.AppendElement(aPromise);
+ return;
+
+ case eRejected:
+ if (mHasError) {
+ JS::Rooted<JS::Value> error(aCx, mErrorToRethrow);
+ aPromise->MaybeReject(error);
+ } else {
+ aPromise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ }
+ return;
+
+ case eResolved:
+ aPromise->MaybeResolveWithUndefined();
+ return;
+ }
+}
+
+void WorkletFetchHandler::RejectPromises(nsresult aResult) {
+ MOZ_ASSERT(mStatus == ePending);
+ MOZ_ASSERT(NS_FAILED(aResult));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mWorklet->Impl()->OnAddModulePromiseSettled();
+
+ for (uint32_t i = 0; i < mPromises.Length(); ++i) {
+ mPromises[i]->MaybeReject(aResult);
+ }
+ mPromises.Clear();
+
+ mStatus = eRejected;
+ mWorklet = nullptr;
+}
+
+void WorkletFetchHandler::RejectPromises(JS::Handle<JS::Value> aValue) {
+ MOZ_ASSERT(mStatus == ePending);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mWorklet->Impl()->OnAddModulePromiseSettled();
+
+ for (uint32_t i = 0; i < mPromises.Length(); ++i) {
+ mPromises[i]->MaybeReject(aValue);
+ }
+ mPromises.Clear();
+
+ mHasError = true;
+ mErrorToRethrow = aValue;
+
+ mozilla::HoldJSObjects(this);
+
+ mStatus = eRejected;
+ mWorklet = nullptr;
+}
+
+void WorkletFetchHandler::ResolvePromises() {
+ MOZ_ASSERT(mStatus == ePending);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mWorklet->Impl()->OnAddModulePromiseSettled();
+
+ for (uint32_t i = 0; i < mPromises.Length(); ++i) {
+ mPromises[i]->MaybeResolveWithUndefined();
+ }
+ mPromises.Clear();
+
+ mStatus = eResolved;
+ mWorklet = nullptr;
+}
+
+nsresult WorkletFetchHandler::StartFetch(JSContext* aCx, nsIURI* aURI,
+ nsIURI* aReferrer) {
+ nsAutoCString spec;
+ nsresult res = aURI->GetSpec(spec);
+ if (NS_WARN_IF(NS_FAILED(res))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RequestOrUSVString requestInput;
+
+ nsAutoString url;
+ CopyUTF8toUTF16(spec, url);
+ requestInput.SetAsUSVString().ShareOrDependUpon(url);
+
+ RootedDictionary<RequestInit> requestInit(aCx);
+ requestInit.mCredentials.Construct(mCredentials);
+
+ // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script
+ // Step 8. mode is "cors"
+ requestInit.mMode.Construct(RequestMode::Cors);
+
+ if (aReferrer) {
+ nsAutoString referrer;
+ res = aReferrer->GetSpec(spec);
+ if (NS_WARN_IF(NS_FAILED(res))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CopyUTF8toUTF16(spec, referrer);
+ requestInit.mReferrer.Construct(referrer);
+ }
+
+ nsCOMPtr<nsIGlobalObject> global =
+ do_QueryInterface(mWorklet->GetParentObject());
+ MOZ_ASSERT(global);
+
+ IgnoredErrorResult rv;
+ SafeRefPtr<Request> request =
+ Request::Constructor(global, aCx, requestInput, requestInit, rv);
+ if (rv.Failed()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ request->OverrideContentPolicyType(mWorklet->Impl()->ContentPolicyType());
+
+ RequestOrUSVString finalRequestInput;
+ finalRequestInput.SetAsRequest() = request.unsafeGetRawPtr();
+
+ RefPtr<Promise> fetchPromise = FetchRequest(
+ global, finalRequestInput, requestInit, CallerType::System, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<WorkletScriptHandler> scriptHandler =
+ new WorkletScriptHandler(mWorklet, aURI);
+ fetchPromise->AppendNativeHandler(scriptHandler);
+ return NS_OK;
+}
+
+void WorkletFetchHandler::HandleFetchFailed(nsIURI* aURI) {
+ nsCOMPtr<nsIRunnable> runnable = new FetchCompleteRunnable(
+ mWorklet->mImpl, aURI, NS_ERROR_FAILURE, nullptr, 0);
+
+ if (NS_WARN_IF(
+ NS_FAILED(mWorklet->mImpl->SendControlMessage(runnable.forget())))) {
+ NS_WARNING("Failed to dispatch FetchCompleteRunnable to a worklet thread.");
+ }
+}
+
+//////////////////////////////////////////////////////////////
+// WorkletScriptHandler
+//////////////////////////////////////////////////////////////
+NS_IMPL_ISUPPORTS(WorkletScriptHandler, nsIStreamLoaderObserver)
+
+WorkletScriptHandler::WorkletScriptHandler(Worklet* aWorklet, nsIURI* aURI)
+ : mWorklet(aWorklet), mURI(aURI) {}
+
+void WorkletScriptHandler::ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aValue.isObject()) {
+ HandleFailure(NS_ERROR_FAILURE);
+ return;
+ }
+
+ RefPtr<Response> response;
+ nsresult rv = UNWRAP_OBJECT(Response, &aValue.toObject(), response);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ HandleFailure(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // https://html.spec.whatwg.org/multipage/worklets.html#dom-worklet-addmodule
+ // Step 6.4.1. If script is null, then:
+ // Step 1.1.2. Reject promise with an "AbortError" DOMException.
+ if (!response->Ok()) {
+ HandleFailure(NS_ERROR_DOM_ABORT_ERR);
+ return;
+ }
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ response->GetBody(getter_AddRefs(inputStream));
+ if (!inputStream) {
+ HandleFailure(NS_ERROR_DOM_NETWORK_ERR);
+ return;
+ }
+
+ nsCOMPtr<nsIInputStreamPump> pump;
+ rv = NS_NewInputStreamPump(getter_AddRefs(pump), inputStream.forget());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ HandleFailure(rv);
+ return;
+ }
+
+ nsCOMPtr<nsIStreamLoader> loader;
+ rv = NS_NewStreamLoader(getter_AddRefs(loader), this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ HandleFailure(rv);
+ return;
+ }
+
+ rv = pump->AsyncRead(loader);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ HandleFailure(rv);
+ return;
+ }
+
+ nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(pump);
+ if (rr) {
+ nsCOMPtr<nsIEventTarget> sts =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ RefPtr<TaskQueue> queue = TaskQueue::Create(
+ sts.forget(), "WorkletScriptHandler STS Delivery Queue");
+ rv = rr->RetargetDeliveryTo(queue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to dispatch the nsIInputStreamPump to a IO thread.");
+ }
+ }
+}
+
+NS_IMETHODIMP WorkletScriptHandler::OnStreamComplete(nsIStreamLoader* aLoader,
+ nsISupports* aContext,
+ nsresult aStatus,
+ uint32_t aStringLen,
+ const uint8_t* aString) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_FAILED(aStatus)) {
+ HandleFailure(aStatus);
+ return NS_OK;
+ }
+
+ // Copy the buffer and decode it on worklet thread, as we can only access
+ // ModuleLoadRequest on worklet thread.
+ UniquePtr<uint8_t[]> scriptTextBuf = MakeUnique<uint8_t[]>(aStringLen);
+ memcpy(scriptTextBuf.get(), aString, aStringLen);
+
+ nsCOMPtr<nsIRunnable> runnable = new FetchCompleteRunnable(
+ mWorklet->mImpl, mURI, NS_OK, std::move(scriptTextBuf), aStringLen);
+
+ if (NS_FAILED(mWorklet->mImpl->SendControlMessage(runnable.forget()))) {
+ HandleFailure(NS_ERROR_FAILURE);
+ }
+
+ return NS_OK;
+}
+
+void WorkletScriptHandler::RejectedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // https://html.spec.whatwg.org/multipage/worklets.html#dom-worklet-addmodule
+ // Step 6.4.1. If script is null, then:
+ // Step 1.1.2. Reject promise with an "AbortError" DOMException.
+ HandleFailure(NS_ERROR_DOM_ABORT_ERR);
+}
+
+void WorkletScriptHandler::HandleFailure(nsresult aResult) {
+ DispatchFetchCompleteToWorklet(aResult);
+}
+
+void WorkletScriptHandler::DispatchFetchCompleteToWorklet(nsresult aRv) {
+ nsCOMPtr<nsIRunnable> runnable =
+ new FetchCompleteRunnable(mWorklet->mImpl, mURI, aRv, nullptr, 0);
+
+ if (NS_WARN_IF(
+ NS_FAILED(mWorklet->mImpl->SendControlMessage(runnable.forget())))) {
+ NS_WARNING("Failed to dispatch FetchCompleteRunnable to a worklet thread.");
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/worklet/WorkletFetchHandler.h b/dom/worklet/WorkletFetchHandler.h
new file mode 100644
index 0000000000..a380caf9eb
--- /dev/null
+++ b/dom/worklet/WorkletFetchHandler.h
@@ -0,0 +1,120 @@
+/* -*- 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_WorkletFetchHandler_h
+#define mozilla_dom_WorkletFetchHandler_h
+
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/RequestBinding.h" // RequestCredentials
+#include "nsIStreamLoader.h"
+
+namespace mozilla::dom {
+class Worklet;
+struct WorkletOptions;
+class WorkletScriptHandler;
+
+namespace loader {
+class AddModuleThrowErrorRunnable;
+} // namespace loader
+
+// WorkletFetchHandler is used to fetch the module scripts on the main thread,
+// and notifies the result of addModule back to |aWorklet|.
+class WorkletFetchHandler final : public nsISupports {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WorkletFetchHandler)
+
+ static already_AddRefed<Promise> AddModule(Worklet* aWorklet, JSContext* aCx,
+ const nsAString& aModuleURL,
+ const WorkletOptions& aOptions,
+ ErrorResult& aRv);
+
+ // Load a module script on main thread.
+ nsresult StartFetch(JSContext* aCx, nsIURI* aURI, nsIURI* aReferrer);
+
+ void ExecutionFailed();
+ void ExecutionFailed(JS::Handle<JS::Value> aError);
+
+ void ExecutionSucceeded();
+
+ void HandleFetchFailed(nsIURI* aURI);
+
+ private:
+ WorkletFetchHandler(Worklet* aWorklet, Promise* aPromise,
+ RequestCredentials aCredentials);
+
+ ~WorkletFetchHandler();
+
+ void AddPromise(JSContext* aCx, Promise* aPromise);
+
+ void RejectPromises(nsresult aResult);
+ void RejectPromises(JS::Handle<JS::Value> aValue);
+
+ void ResolvePromises();
+
+ friend class StartFetchRunnable;
+ friend class loader::AddModuleThrowErrorRunnable;
+ RefPtr<Worklet> mWorklet;
+ nsTArray<RefPtr<Promise>> mPromises;
+
+ enum { ePending, eRejected, eResolved } mStatus;
+
+ RequestCredentials mCredentials;
+
+ bool mHasError = false;
+ JS::Heap<JS::Value> mErrorToRethrow;
+};
+
+// A Runnable to call WorkletFetchHandler::StartFetch on the main thread.
+class StartFetchRunnable final : public Runnable {
+ public:
+ StartFetchRunnable(
+ const nsMainThreadPtrHandle<WorkletFetchHandler>& aHandlerRef,
+ nsIURI* aURI, nsIURI* aReferrer);
+ ~StartFetchRunnable() = default;
+
+ NS_IMETHOD
+ Run() override;
+
+ private:
+ nsMainThreadPtrHandle<WorkletFetchHandler> mHandlerRef;
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsIURI> mReferrer;
+};
+
+// WorkletScriptHandler is used to handle the result of fetching the module
+// script.
+class WorkletScriptHandler final : public PromiseNativeHandler,
+ public nsIStreamLoaderObserver {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ WorkletScriptHandler(Worklet* aWorklet, nsIURI* aURI);
+
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ NS_IMETHOD
+ OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext,
+ nsresult aStatus, uint32_t aStringLen,
+ const uint8_t* aString) override;
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ void HandleFailure(nsresult aResult);
+
+ private:
+ ~WorkletScriptHandler() = default;
+
+ void DispatchFetchCompleteToWorklet(nsresult aRv);
+
+ RefPtr<Worklet> mWorklet;
+ nsCOMPtr<nsIURI> mURI;
+};
+
+} // namespace mozilla::dom
+#endif // mozilla_dom_WorkletFetchHandler_h
diff --git a/dom/worklet/WorkletGlobalScope.cpp b/dom/worklet/WorkletGlobalScope.cpp
new file mode 100644
index 0000000000..9231f6b095
--- /dev/null
+++ b/dom/worklet/WorkletGlobalScope.cpp
@@ -0,0 +1,116 @@
+/* -*- 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 "WorkletGlobalScope.h"
+#include "mozilla/dom/WorkletGlobalScopeBinding.h"
+#include "mozilla/dom/WorkletImpl.h"
+#include "mozilla/dom/WorkletThread.h"
+#include "mozilla/dom/worklet/WorkletModuleLoader.h"
+#include "mozilla/dom/Console.h"
+#include "nsContentUtils.h"
+#include "nsJSUtils.h"
+
+using JS::loader::ModuleLoaderBase;
+using mozilla::dom::loader::WorkletModuleLoader;
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(WorkletGlobalScope)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WorkletGlobalScope)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsole)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mModuleLoader)
+ tmp->UnlinkObjectsInGlobal();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WorkletGlobalScope)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsole)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mModuleLoader)
+ tmp->TraverseObjectsInGlobal(cb);
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(WorkletGlobalScope)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(WorkletGlobalScope)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkletGlobalScope)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsIGlobalObject)
+ NS_INTERFACE_MAP_ENTRY(WorkletGlobalScope)
+NS_INTERFACE_MAP_END
+
+WorkletGlobalScope::WorkletGlobalScope(WorkletImpl* aImpl)
+ : mImpl(aImpl), mCreationTimeStamp(TimeStamp::Now()) {}
+
+WorkletGlobalScope::~WorkletGlobalScope() = default;
+
+JSObject* WorkletGlobalScope::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ MOZ_CRASH("We should never get here!");
+ return nullptr;
+}
+
+already_AddRefed<Console> WorkletGlobalScope::GetConsole(JSContext* aCx,
+ ErrorResult& aRv) {
+ if (!mConsole) {
+ MOZ_ASSERT(Impl());
+ const WorkletLoadInfo& loadInfo = Impl()->LoadInfo();
+ mConsole = Console::CreateForWorklet(aCx, this, loadInfo.OuterWindowID(),
+ loadInfo.InnerWindowID(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ }
+
+ RefPtr<Console> console = mConsole;
+ return console.forget();
+}
+
+void WorkletGlobalScope::InitModuleLoader(WorkletModuleLoader* aModuleLoader) {
+ MOZ_ASSERT(!mModuleLoader);
+ mModuleLoader = aModuleLoader;
+}
+
+ModuleLoaderBase* WorkletGlobalScope::GetModuleLoader(JSContext* aCx) {
+ return mModuleLoader;
+};
+
+OriginTrials WorkletGlobalScope::Trials() const { return mImpl->Trials(); }
+
+Maybe<nsID> WorkletGlobalScope::GetAgentClusterId() const {
+ return mImpl->GetAgentClusterId();
+}
+
+bool WorkletGlobalScope::IsSharedMemoryAllowed() const {
+ return mImpl->IsSharedMemoryAllowed();
+}
+
+bool WorkletGlobalScope::ShouldResistFingerprinting(RFPTarget aTarget) const {
+ return mImpl->ShouldResistFingerprinting(aTarget);
+}
+
+void WorkletGlobalScope::Dump(const Optional<nsAString>& aString) const {
+ WorkletThread::AssertIsOnWorkletThread();
+
+ if (!nsJSUtils::DumpEnabled()) {
+ return;
+ }
+
+ if (!aString.WasPassed()) {
+ return;
+ }
+
+ NS_ConvertUTF16toUTF8 str(aString.Value());
+
+ MOZ_LOG(nsContentUtils::DOMDumpLog(), mozilla::LogLevel::Debug,
+ ("[Worklet.Dump] %s", str.get()));
+#ifdef ANDROID
+ __android_log_print(ANDROID_LOG_INFO, "Gecko", "%s", str.get());
+#endif
+ fputs(str.get(), stdout);
+ fflush(stdout);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/worklet/WorkletGlobalScope.h b/dom/worklet/WorkletGlobalScope.h
new file mode 100644
index 0000000000..c053d57331
--- /dev/null
+++ b/dom/worklet/WorkletGlobalScope.h
@@ -0,0 +1,102 @@
+/* -*- 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_WorkletGlobalScope_h
+#define mozilla_dom_WorkletGlobalScope_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsIGlobalObject.h"
+#include "nsWrapperCache.h"
+
+#define WORKLET_IID \
+ { \
+ 0x1b3f62e7, 0xe357, 0x44be, { \
+ 0xbf, 0xe0, 0xdf, 0x85, 0xe6, 0x56, 0x85, 0xac \
+ } \
+ }
+
+namespace JS::loader {
+class ModuleLoaderBase;
+}
+
+namespace mozilla {
+
+class ErrorResult;
+class WorkletImpl;
+
+namespace dom {
+
+namespace loader {
+class WorkletModuleLoader;
+}
+
+class Console;
+
+class WorkletGlobalScope : public nsIGlobalObject, public nsWrapperCache {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(WORKLET_IID)
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(WorkletGlobalScope)
+
+ WorkletGlobalScope(WorkletImpl*);
+
+ nsIGlobalObject* GetParentObject() const { return nullptr; }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual bool WrapGlobalObject(JSContext* aCx,
+ JS::MutableHandle<JSObject*> aReflector) = 0;
+
+ JSObject* GetGlobalJSObject() override { return GetWrapper(); }
+ JSObject* GetGlobalJSObjectPreserveColor() const override {
+ return GetWrapperPreserveColor();
+ }
+
+ already_AddRefed<Console> GetConsole(JSContext* aCx, ErrorResult& aRv);
+
+ WorkletImpl* Impl() const { return mImpl.get(); }
+
+ void Dump(const Optional<nsAString>& aString) const;
+
+ DOMHighResTimeStamp TimeStampToDOMHighRes(const TimeStamp& aTimeStamp) const {
+ MOZ_ASSERT(!aTimeStamp.IsNull());
+ TimeDuration duration = aTimeStamp - mCreationTimeStamp;
+ return duration.ToMilliseconds();
+ }
+
+ void InitModuleLoader(loader::WorkletModuleLoader* aModuleLoader);
+
+ JS::loader::ModuleLoaderBase* GetModuleLoader(
+ JSContext* aCx = nullptr) override;
+
+ OriginTrials Trials() const override;
+ Maybe<nsID> GetAgentClusterId() const override;
+ bool IsSharedMemoryAllowed() const override;
+ bool ShouldResistFingerprinting(RFPTarget aTarget) const override;
+
+ protected:
+ ~WorkletGlobalScope();
+
+ const RefPtr<WorkletImpl> mImpl;
+
+ private:
+ TimeStamp mCreationTimeStamp;
+ RefPtr<Console> mConsole;
+ RefPtr<loader::WorkletModuleLoader> mModuleLoader;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(WorkletGlobalScope, WORKLET_IID)
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_WorkletGlobalScope_h
diff --git a/dom/worklet/WorkletImpl.cpp b/dom/worklet/WorkletImpl.cpp
new file mode 100644
index 0000000000..8b54f4d570
--- /dev/null
+++ b/dom/worklet/WorkletImpl.cpp
@@ -0,0 +1,152 @@
+/* -*- 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 https://mozilla.org/MPL/2.0/. */
+
+#include "WorkletImpl.h"
+
+#include "Worklet.h"
+#include "WorkletThread.h"
+
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/dom/DocGroup.h"
+#include "mozilla/dom/RegisterWorkletBindings.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/WorkletBinding.h"
+#include "mozilla/dom/WorkletGlobalScope.h"
+#include "mozilla/dom/worklet/WorkletModuleLoader.h"
+#include "nsGlobalWindowInner.h"
+
+using mozilla::dom::loader::WorkletModuleLoader;
+using mozilla::dom::loader::WorkletScriptLoader;
+
+namespace mozilla {
+// ---------------------------------------------------------------------------
+// WorkletLoadInfo
+
+WorkletLoadInfo::WorkletLoadInfo(nsPIDOMWindowInner* aWindow)
+ : mInnerWindowID(aWindow->WindowID()) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsPIDOMWindowOuter* outerWindow = aWindow->GetOuterWindow();
+ if (outerWindow) {
+ mOuterWindowID = outerWindow->WindowID();
+ } else {
+ mOuterWindowID = 0;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// WorkletImpl
+
+WorkletImpl::WorkletImpl(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal)
+ : mPrincipal(NullPrincipal::CreateWithInheritedAttributes(aPrincipal)),
+ mWorkletLoadInfo(aWindow),
+ mTerminated(false),
+ mFinishedOnExecutionThread(false),
+ mTrials(OriginTrials::FromWindow(nsGlobalWindowInner::Cast(aWindow))) {
+ Unused << NS_WARN_IF(
+ NS_FAILED(ipc::PrincipalToPrincipalInfo(mPrincipal, &mPrincipalInfo)));
+
+ if (aWindow->GetDocGroup()) {
+ mAgentClusterId.emplace(aWindow->GetDocGroup()->AgentClusterId());
+ }
+
+ mSharedMemoryAllowed =
+ nsGlobalWindowInner::Cast(aWindow)->IsSharedMemoryAllowed();
+
+ mShouldResistFingerprinting = aWindow->AsGlobal()->ShouldResistFingerprinting(
+ RFPTarget::IsAlwaysEnabledForPrecompute);
+}
+
+WorkletImpl::~WorkletImpl() { MOZ_ASSERT(!mGlobalScope); }
+
+JSObject* WorkletImpl::WrapWorklet(JSContext* aCx, dom::Worklet* aWorklet,
+ JS::Handle<JSObject*> aGivenProto) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return dom::Worklet_Binding::Wrap(aCx, aWorklet, aGivenProto);
+}
+
+dom::WorkletGlobalScope* WorkletImpl::GetGlobalScope() {
+ dom::WorkletThread::AssertIsOnWorkletThread();
+
+ if (mGlobalScope) {
+ return mGlobalScope;
+ }
+ if (mFinishedOnExecutionThread) {
+ return nullptr;
+ }
+
+ dom::AutoJSAPI jsapi;
+ jsapi.Init();
+ JSContext* cx = jsapi.cx();
+
+ mGlobalScope = ConstructGlobalScope();
+
+ JS::Rooted<JSObject*> global(cx);
+ NS_ENSURE_TRUE(mGlobalScope->WrapGlobalObject(cx, &global), nullptr);
+
+ JSAutoRealm ar(cx, global);
+
+ // Init Web IDL bindings
+ if (!dom::RegisterWorkletBindings(cx, global)) {
+ return nullptr;
+ }
+
+ JS_FireOnNewGlobalObject(cx, global);
+
+ MOZ_ASSERT(!mGlobalScope->GetModuleLoader(cx));
+
+ RefPtr<WorkletScriptLoader> scriptLoader = new WorkletScriptLoader();
+ RefPtr<WorkletModuleLoader> moduleLoader =
+ new WorkletModuleLoader(scriptLoader, mGlobalScope);
+ mGlobalScope->InitModuleLoader(moduleLoader);
+
+ return mGlobalScope;
+}
+
+void WorkletImpl::NotifyWorkletFinished() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mTerminated) {
+ return;
+ }
+
+ // Release global scope on its thread.
+ SendControlMessage(
+ NS_NewRunnableFunction("WorkletImpl::NotifyWorkletFinished",
+ [self = RefPtr<WorkletImpl>(this)]() {
+ self->mFinishedOnExecutionThread = true;
+ self->mGlobalScope = nullptr;
+ }));
+
+ mTerminated = true;
+ if (mWorkletThread) {
+ mWorkletThread->Terminate();
+ mWorkletThread = nullptr;
+ }
+}
+
+nsresult WorkletImpl::SendControlMessage(
+ already_AddRefed<nsIRunnable> aRunnable) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<nsIRunnable> runnable = std::move(aRunnable);
+
+ // TODO: bug 1492011 re ConsoleWorkletRunnable.
+ if (mTerminated) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+
+ if (!mWorkletThread) {
+ // Thread creation. FIXME: this will change.
+ mWorkletThread = dom::WorkletThread::Create(this);
+ if (!mWorkletThread) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+ }
+
+ return mWorkletThread->DispatchRunnable(runnable.forget());
+}
+
+} // namespace mozilla
diff --git a/dom/worklet/WorkletImpl.h b/dom/worklet/WorkletImpl.h
new file mode 100644
index 0000000000..2bc8742ab5
--- /dev/null
+++ b/dom/worklet/WorkletImpl.h
@@ -0,0 +1,131 @@
+/* -*- 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 https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_worklet_WorkletImpl_h
+#define mozilla_dom_worklet_WorkletImpl_h
+
+#include "MainThreadUtils.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/OriginTrials.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "nsRFPService.h"
+
+class nsPIDOMWindowInner;
+class nsIPrincipal;
+class nsIRunnable;
+
+namespace mozilla {
+
+namespace dom {
+
+class Worklet;
+class WorkletGlobalScope;
+class WorkletThread;
+
+} // namespace dom
+
+class WorkletLoadInfo {
+ public:
+ explicit WorkletLoadInfo(nsPIDOMWindowInner* aWindow);
+
+ uint64_t OuterWindowID() const { return mOuterWindowID; }
+ uint64_t InnerWindowID() const { return mInnerWindowID; }
+
+ private:
+ // Modified only in constructor.
+ uint64_t mOuterWindowID;
+ const uint64_t mInnerWindowID;
+};
+
+/**
+ * WorkletImpl is accessed from both the worklet's parent thread (on which the
+ * Worklet object lives) and the worklet's execution thread. It is owned by
+ * Worklet and WorkletGlobalScope. No parent thread cycle collected objects
+ * are owned indefinitely by WorkletImpl because WorkletImpl is not cycle
+ * collected.
+ */
+class WorkletImpl {
+ using RFPTarget = mozilla::RFPTarget;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WorkletImpl);
+
+ // Methods for parent thread only:
+
+ virtual JSObject* WrapWorklet(JSContext* aCx, dom::Worklet* aWorklet,
+ JS::Handle<JSObject*> aGivenProto);
+
+ virtual nsresult SendControlMessage(already_AddRefed<nsIRunnable> aRunnable);
+
+ void NotifyWorkletFinished();
+
+ virtual nsContentPolicyType ContentPolicyType() const = 0;
+
+ // Execution thread only.
+ dom::WorkletGlobalScope* GetGlobalScope();
+
+ // Any thread.
+
+ const OriginTrials& Trials() const { return mTrials; }
+ const WorkletLoadInfo& LoadInfo() const { return mWorkletLoadInfo; }
+ const OriginAttributes& OriginAttributesRef() const {
+ return mPrincipal->OriginAttributesRef();
+ }
+ nsIPrincipal* Principal() const { return mPrincipal; }
+ const ipc::PrincipalInfo& PrincipalInfo() const { return mPrincipalInfo; }
+
+ const Maybe<nsID>& GetAgentClusterId() const { return mAgentClusterId; }
+
+ bool IsSharedMemoryAllowed() const { return mSharedMemoryAllowed; }
+ bool IsSystemPrincipal() const { return mPrincipal->IsSystemPrincipal(); }
+ bool ShouldResistFingerprinting(RFPTarget aTarget) const {
+ return mShouldResistFingerprinting &&
+ nsRFPService::IsRFPEnabledFor(aTarget);
+ }
+
+ virtual void OnAddModuleStarted() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ // empty base impl
+ }
+
+ virtual void OnAddModulePromiseSettled() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ // empty base impl
+ }
+
+ protected:
+ WorkletImpl(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal);
+ virtual ~WorkletImpl();
+
+ virtual already_AddRefed<dom::WorkletGlobalScope> ConstructGlobalScope() = 0;
+
+ // Modified only in constructor.
+ ipc::PrincipalInfo mPrincipalInfo;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+
+ const WorkletLoadInfo mWorkletLoadInfo;
+
+ // Parent thread only.
+ RefPtr<dom::WorkletThread> mWorkletThread;
+ bool mTerminated;
+
+ // Execution thread only.
+ RefPtr<dom::WorkletGlobalScope> mGlobalScope;
+ bool mFinishedOnExecutionThread;
+
+ Maybe<nsID> mAgentClusterId;
+
+ bool mSharedMemoryAllowed;
+ bool mShouldResistFingerprinting;
+
+ const OriginTrials mTrials;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_dom_worklet_WorkletImpl_h
diff --git a/dom/worklet/WorkletThread.cpp b/dom/worklet/WorkletThread.cpp
new file mode 100644
index 0000000000..c5ad7070b0
--- /dev/null
+++ b/dom/worklet/WorkletThread.cpp
@@ -0,0 +1,474 @@
+/* -*- 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 "WorkletThread.h"
+#include "prthread.h"
+#include "nsContentUtils.h"
+#include "nsCycleCollector.h"
+#include "nsJSEnvironment.h"
+#include "nsJSPrincipals.h"
+#include "mozilla/dom/AtomList.h"
+#include "mozilla/dom/WorkletGlobalScope.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/CycleCollectedJSRuntime.h"
+#include "mozilla/EventQueue.h"
+#include "mozilla/ThreadEventQueue.h"
+#include "js/ContextOptions.h"
+#include "js/Exception.h"
+#include "js/Initialization.h"
+#include "XPCSelfHostedShmem.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+// The size of the worklet runtime heaps in bytes.
+#define WORKLET_DEFAULT_RUNTIME_HEAPSIZE 32 * 1024 * 1024
+
+// The C stack size. We use the same stack size on all platforms for
+// consistency.
+const uint32_t kWorkletStackSize = 256 * sizeof(size_t) * 1024;
+
+// Half the size of the actual C stack, to be safe.
+#define WORKLET_CONTEXT_NATIVE_STACK_LIMIT 128 * sizeof(size_t) * 1024
+
+// Helper functions
+
+bool PreserveWrapper(JSContext* aCx, JS::Handle<JSObject*> aObj) {
+ MOZ_ASSERT(aCx);
+ MOZ_ASSERT(aObj);
+ MOZ_ASSERT(mozilla::dom::IsDOMObject(aObj));
+ return mozilla::dom::TryPreserveWrapper(aObj);
+}
+
+JSObject* Wrap(JSContext* aCx, JS::Handle<JSObject*> aExisting,
+ JS::Handle<JSObject*> aObj) {
+ if (aExisting) {
+ js::Wrapper::Renew(aExisting, aObj,
+ &js::OpaqueCrossCompartmentWrapper::singleton);
+ }
+
+ return js::Wrapper::New(aCx, aObj,
+ &js::OpaqueCrossCompartmentWrapper::singleton);
+}
+
+const JSWrapObjectCallbacks WrapObjectCallbacks = {
+ Wrap,
+ nullptr,
+};
+
+} // namespace
+
+// This classes control CC in the worklet thread.
+
+class WorkletJSRuntime final : public mozilla::CycleCollectedJSRuntime {
+ public:
+ explicit WorkletJSRuntime(JSContext* aCx) : CycleCollectedJSRuntime(aCx) {}
+
+ ~WorkletJSRuntime() override = default;
+
+ virtual void PrepareForForgetSkippable() override {}
+
+ virtual void BeginCycleCollectionCallback(
+ mozilla::CCReason aReason) override {}
+
+ virtual void EndCycleCollectionCallback(
+ CycleCollectorResults& aResults) override {}
+
+ virtual void DispatchDeferredDeletion(bool aContinuation,
+ bool aPurge) override {
+ MOZ_ASSERT(!aContinuation);
+ nsCycleCollector_doDeferredDeletion();
+ }
+
+ virtual void CustomGCCallback(JSGCStatus aStatus) override {
+ // nsCycleCollector_collect() requires a cycle collector but
+ // ~WorkletJSContext calls nsCycleCollector_shutdown() and the base class
+ // destructor will trigger a final GC. The nsCycleCollector_collect()
+ // call can be skipped in this GC as ~CycleCollectedJSContext removes the
+ // context from |this|.
+ if (aStatus == JSGC_END && GetContext()) {
+ nsCycleCollector_collect(CCReason::GC_FINISHED, nullptr);
+ }
+ }
+};
+
+class WorkletJSContext final : public CycleCollectedJSContext {
+ public:
+ WorkletJSContext() {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsCycleCollector_startup();
+ }
+
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY because otherwise we have to annotate the
+ // SpiderMonkey JS::JobQueue's destructor as MOZ_CAN_RUN_SCRIPT, which is a
+ // bit of a pain.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY ~WorkletJSContext() override {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ JSContext* cx = MaybeContext();
+ if (!cx) {
+ return; // Initialize() must have failed
+ }
+
+ nsCycleCollector_shutdown();
+ }
+
+ WorkletJSContext* GetAsWorkletJSContext() override { return this; }
+
+ CycleCollectedJSRuntime* CreateRuntime(JSContext* aCx) override {
+ return new WorkletJSRuntime(aCx);
+ }
+
+ nsresult Initialize(JSRuntime* aParentRuntime) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsresult rv = CycleCollectedJSContext::Initialize(
+ aParentRuntime, WORKLET_DEFAULT_RUNTIME_HEAPSIZE);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ JSContext* cx = Context();
+
+ js::SetPreserveWrapperCallbacks(cx, PreserveWrapper, HasReleasedWrapper);
+ JS_InitDestroyPrincipalsCallback(cx, nsJSPrincipals::Destroy);
+ JS_InitReadPrincipalsCallback(cx, nsJSPrincipals::ReadPrincipals);
+ JS_SetWrapObjectCallbacks(cx, &WrapObjectCallbacks);
+ JS_SetFutexCanWait(cx);
+
+ return NS_OK;
+ }
+
+ void DispatchToMicroTask(
+ already_AddRefed<MicroTaskRunnable> aRunnable) override {
+ RefPtr<MicroTaskRunnable> runnable(aRunnable);
+
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(runnable);
+
+ JSContext* cx = Context();
+ MOZ_ASSERT(cx);
+
+#ifdef DEBUG
+ JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
+ MOZ_ASSERT(global);
+#endif
+
+ JS::JobQueueMayNotBeEmpty(cx);
+ GetMicroTaskQueue().push_back(std::move(runnable));
+ }
+
+ bool IsSystemCaller() const override {
+ // Currently no support for special system worklet privileges.
+ return false;
+ }
+
+ void ReportError(JSErrorReport* aReport,
+ JS::ConstUTF8CharsZ aToStringResult) override;
+
+ uint64_t GetCurrentWorkletWindowID() {
+ JSObject* global = JS::CurrentGlobalOrNull(Context());
+ if (NS_WARN_IF(!global)) {
+ return 0;
+ }
+ nsIGlobalObject* nativeGlobal = xpc::NativeGlobal(global);
+ nsCOMPtr<WorkletGlobalScope> workletGlobal =
+ do_QueryInterface(nativeGlobal);
+ if (NS_WARN_IF(!workletGlobal)) {
+ return 0;
+ }
+ return workletGlobal->Impl()->LoadInfo().InnerWindowID();
+ }
+};
+
+void WorkletJSContext::ReportError(JSErrorReport* aReport,
+ JS::ConstUTF8CharsZ aToStringResult) {
+ RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport();
+ xpcReport->Init(aReport, aToStringResult.c_str(), IsSystemCaller(),
+ GetCurrentWorkletWindowID());
+ RefPtr<AsyncErrorReporter> reporter = new AsyncErrorReporter(xpcReport);
+
+ JSContext* cx = Context();
+ if (JS_IsExceptionPending(cx)) {
+ JS::ExceptionStack exnStack(cx);
+ if (JS::StealPendingExceptionStack(cx, &exnStack)) {
+ JS::Rooted<JSObject*> stack(cx);
+ JS::Rooted<JSObject*> stackGlobal(cx);
+ xpc::FindExceptionStackForConsoleReport(nullptr, exnStack.exception(),
+ exnStack.stack(), &stack,
+ &stackGlobal);
+ if (stack) {
+ reporter->SerializeStack(cx, stack);
+ }
+ }
+ }
+
+ NS_DispatchToMainThread(reporter);
+}
+
+// This is the first runnable to be dispatched. It calls the RunEventLoop() so
+// basically everything happens into this runnable. The reason behind this
+// approach is that, when the Worklet is terminated, it must not have any JS in
+// stack, but, because we have CC, nsIThread creates an AutoNoJSAPI object by
+// default. Using this runnable, CC exists only into it.
+class WorkletThread::PrimaryRunnable final : public Runnable {
+ public:
+ explicit PrimaryRunnable(WorkletThread* aWorkletThread)
+ : Runnable("WorkletThread::PrimaryRunnable"),
+ mWorkletThread(aWorkletThread) {
+ MOZ_ASSERT(aWorkletThread);
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ NS_IMETHOD
+ Run() override {
+ mWorkletThread->RunEventLoop();
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<WorkletThread> mWorkletThread;
+};
+
+// This is the last runnable to be dispatched. It calls the TerminateInternal()
+class WorkletThread::TerminateRunnable final : public Runnable {
+ public:
+ explicit TerminateRunnable(WorkletThread* aWorkletThread)
+ : Runnable("WorkletThread::TerminateRunnable"),
+ mWorkletThread(aWorkletThread) {
+ MOZ_ASSERT(aWorkletThread);
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ NS_IMETHOD
+ Run() override {
+ mWorkletThread->TerminateInternal();
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<WorkletThread> mWorkletThread;
+};
+
+WorkletThread::WorkletThread(WorkletImpl* aWorkletImpl)
+ : nsThread(
+ MakeNotNull<ThreadEventQueue*>(MakeUnique<mozilla::EventQueue>()),
+ nsThread::NOT_MAIN_THREAD, {.stackSize = kWorkletStackSize}),
+ mWorkletImpl(aWorkletImpl),
+ mExitLoop(false),
+ mIsTerminating(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsContentUtils::RegisterShutdownObserver(this);
+}
+
+WorkletThread::~WorkletThread() = default;
+
+// static
+already_AddRefed<WorkletThread> WorkletThread::Create(
+ WorkletImpl* aWorkletImpl) {
+ RefPtr<WorkletThread> thread = new WorkletThread(aWorkletImpl);
+ if (NS_WARN_IF(NS_FAILED(thread->Init("DOM Worklet"_ns)))) {
+ return nullptr;
+ }
+
+ RefPtr<PrimaryRunnable> runnable = new PrimaryRunnable(thread);
+ if (NS_WARN_IF(NS_FAILED(thread->DispatchRunnable(runnable.forget())))) {
+ return nullptr;
+ }
+
+ return thread.forget();
+}
+
+nsresult WorkletThread::DispatchRunnable(
+ already_AddRefed<nsIRunnable> aRunnable) {
+ nsCOMPtr<nsIRunnable> runnable(aRunnable);
+ return nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+}
+
+NS_IMETHODIMP
+WorkletThread::DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) {
+ nsCOMPtr<nsIRunnable> runnable(aRunnable);
+ return Dispatch(runnable.forget(), aFlags);
+}
+
+NS_IMETHODIMP
+WorkletThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable,
+ uint32_t aFlags) {
+ nsCOMPtr<nsIRunnable> runnable(aRunnable);
+
+ // Worklet only supports asynchronous dispatch.
+ if (NS_WARN_IF(aFlags != NS_DISPATCH_NORMAL)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ return nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+}
+
+NS_IMETHODIMP
+WorkletThread::DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t aFlags) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+static bool DispatchToEventLoop(void* aClosure,
+ JS::Dispatchable* aDispatchable) {
+ // This callback may execute either on the worklet thread or a random
+ // JS-internal helper thread.
+
+ // See comment at JS::InitDispatchToEventLoop() below for how we know the
+ // thread is alive.
+ nsIThread* thread = static_cast<nsIThread*>(aClosure);
+
+ nsresult rv = thread->Dispatch(
+ NS_NewRunnableFunction(
+ "WorkletThread::DispatchToEventLoop",
+ [aDispatchable]() {
+ CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get();
+ if (!ccjscx) {
+ return;
+ }
+
+ WorkletJSContext* wjc = ccjscx->GetAsWorkletJSContext();
+ if (!wjc) {
+ return;
+ }
+
+ aDispatchable->run(wjc->Context(),
+ JS::Dispatchable::NotShuttingDown);
+ }),
+ NS_DISPATCH_NORMAL);
+
+ return NS_SUCCEEDED(rv);
+}
+
+// static
+void WorkletThread::EnsureCycleCollectedJSContext(
+ JSRuntime* aParentRuntime, const JS::ContextOptions& aOptions) {
+ CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get();
+ if (ccjscx) {
+ MOZ_ASSERT(ccjscx->GetAsWorkletJSContext());
+ return;
+ }
+
+ WorkletJSContext* context = new WorkletJSContext();
+ nsresult rv = context->Initialize(aParentRuntime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // TODO: error propagation
+ return;
+ }
+
+ JS::ContextOptionsRef(context->Context()) = aOptions;
+
+ JS_SetGCParameter(context->Context(), JSGC_MAX_BYTES, uint32_t(-1));
+
+ // FIXME: JS_SetDefaultLocale
+ // FIXME: JSSettings
+ // FIXME: JS_SetSecurityCallbacks
+ // FIXME: JS::SetAsyncTaskCallbacks
+ // FIXME: JS::SetCTypesActivityCallback
+ // FIXME: JS_SetGCZeal
+
+ // A thread lives strictly longer than its JSRuntime so we can safely
+ // store a raw pointer as the callback's closure argument on the JSRuntime.
+ JS::InitDispatchToEventLoop(context->Context(), DispatchToEventLoop,
+ NS_GetCurrentThread());
+
+ JS_SetNativeStackQuota(context->Context(),
+ WORKLET_CONTEXT_NATIVE_STACK_LIMIT);
+
+ // When available, set the self-hosted shared memory to be read, so that we
+ // can decode the self-hosted content instead of parsing it.
+ auto& shm = xpc::SelfHostedShmem::GetSingleton();
+ JS::SelfHostedCache selfHostedContent = shm.Content();
+
+ if (!JS::InitSelfHostedCode(context->Context(), selfHostedContent)) {
+ // TODO: error propagation
+ return;
+ }
+}
+
+void WorkletThread::RunEventLoop() {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ PR_SetCurrentThreadName("worklet");
+
+ while (!mExitLoop) {
+ MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(this, /* wait: */ true));
+ }
+
+ DeleteCycleCollectedJSContext();
+}
+
+void WorkletThread::Terminate() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mIsTerminating) {
+ // nsThread::Dispatch() would leak the runnable if the event queue is no
+ // longer accepting runnables.
+ return;
+ }
+
+ mIsTerminating = true;
+
+ nsContentUtils::UnregisterShutdownObserver(this);
+
+ RefPtr<TerminateRunnable> runnable = new TerminateRunnable(this);
+ DispatchRunnable(runnable.forget());
+}
+
+void WorkletThread::TerminateInternal() {
+ MOZ_ASSERT(!CycleCollectedJSContext::Get() || IsOnWorkletThread());
+
+ mExitLoop = true;
+
+ nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod(
+ "WorkletThread::Shutdown", this, &WorkletThread::Shutdown);
+ NS_DispatchToMainThread(runnable);
+}
+
+/* static */
+void WorkletThread::DeleteCycleCollectedJSContext() {
+ CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get();
+ if (!ccjscx) {
+ return;
+ }
+
+ // Release any MessagePort kept alive by its ipc actor.
+ mozilla::ipc::BackgroundChild::CloseForCurrentThread();
+
+ WorkletJSContext* workletjscx = ccjscx->GetAsWorkletJSContext();
+ MOZ_ASSERT(workletjscx);
+ delete workletjscx;
+}
+
+/* static */
+bool WorkletThread::IsOnWorkletThread() {
+ CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get();
+ return ccjscx && ccjscx->GetAsWorkletJSContext();
+}
+
+/* static */
+void WorkletThread::AssertIsOnWorkletThread() {
+ MOZ_ASSERT(IsOnWorkletThread());
+}
+
+// nsIObserver
+NS_IMETHODIMP
+WorkletThread::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t*) {
+ MOZ_ASSERT(strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0);
+
+ // The WorkletImpl will terminate the worklet thread after sending a message
+ // to release worklet thread objects.
+ mWorkletImpl->NotifyWorkletFinished();
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(WorkletThread, nsThread, nsIObserver)
+
+} // namespace mozilla::dom
diff --git a/dom/worklet/WorkletThread.h b/dom/worklet/WorkletThread.h
new file mode 100644
index 0000000000..e6d0e414dc
--- /dev/null
+++ b/dom/worklet/WorkletThread.h
@@ -0,0 +1,78 @@
+/* -*- 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_worklet_WorkletThread_h
+#define mozilla_dom_worklet_WorkletThread_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/CondVar.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/WorkletImpl.h"
+#include "nsIObserver.h"
+#include "nsThread.h"
+
+class nsIRunnable;
+
+namespace JS {
+class ContextOptions;
+}; // namespace JS
+
+namespace mozilla::dom {
+
+class WorkletThread final : public nsThread, public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIOBSERVER
+
+ static already_AddRefed<WorkletThread> Create(WorkletImpl* aWorkletImpl);
+
+ // Threads that call EnsureCycleCollectedJSContext must call
+ // DeleteCycleCollectedJSContext::Get() before terminating. Clients of
+ // Create() do not need to do this as Terminate() will ensure this happens.
+ static void EnsureCycleCollectedJSContext(JSRuntime* aParentRuntime,
+ const JS::ContextOptions& aOptions);
+ static void DeleteCycleCollectedJSContext();
+
+ static bool IsOnWorkletThread();
+
+ static void AssertIsOnWorkletThread();
+
+ nsresult DispatchRunnable(already_AddRefed<nsIRunnable> aRunnable);
+
+ void Terminate();
+
+ private:
+ explicit WorkletThread(WorkletImpl* aWorkletImpl);
+ ~WorkletThread();
+
+ void RunEventLoop();
+ class PrimaryRunnable;
+
+ void TerminateInternal();
+ class TerminateRunnable;
+
+ // This should only be called by consumers that have an
+ // nsIEventTarget/nsIThread pointer.
+ NS_IMETHOD
+ Dispatch(already_AddRefed<nsIRunnable> aRunnable, uint32_t aFlags) override;
+
+ NS_IMETHOD
+ DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) override;
+
+ NS_IMETHOD
+ DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) override;
+
+ const RefPtr<WorkletImpl> mWorkletImpl;
+
+ bool mExitLoop; // worklet execution thread
+
+ bool mIsTerminating; // main thread
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_worklet_WorkletThread_h
diff --git a/dom/worklet/loader/WorkletModuleLoader.cpp b/dom/worklet/loader/WorkletModuleLoader.cpp
new file mode 100644
index 0000000000..34353107f8
--- /dev/null
+++ b/dom/worklet/loader/WorkletModuleLoader.cpp
@@ -0,0 +1,297 @@
+/* -*- 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 "WorkletModuleLoader.h"
+
+#include "js/CompileOptions.h" // JS::InstantiateOptions
+#include "js/experimental/JSStencil.h" // JS::CompileModuleScriptToStencil, JS::InstantiateModuleStencil
+#include "js/loader/ModuleLoadRequest.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/StructuredCloneHolder.h"
+#include "mozilla/dom/Worklet.h"
+#include "mozilla/dom/WorkletFetchHandler.h"
+#include "nsStringBundle.h"
+
+using JS::loader::ModuleLoadRequest;
+using JS::loader::ResolveError;
+
+namespace mozilla::dom::loader {
+
+//////////////////////////////////////////////////////////////
+// WorkletScriptLoader
+//////////////////////////////////////////////////////////////
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkletScriptLoader)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(WorkletScriptLoader)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(WorkletScriptLoader)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(WorkletScriptLoader)
+
+//////////////////////////////////////////////////////////////
+// WorkletModuleLoader
+//////////////////////////////////////////////////////////////
+
+NS_IMPL_ADDREF_INHERITED(WorkletModuleLoader, ModuleLoaderBase)
+NS_IMPL_RELEASE_INHERITED(WorkletModuleLoader, ModuleLoaderBase)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(WorkletModuleLoader, ModuleLoaderBase,
+ mFetchingRequests)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkletModuleLoader)
+NS_INTERFACE_MAP_END_INHERITING(ModuleLoaderBase)
+
+WorkletModuleLoader::WorkletModuleLoader(WorkletScriptLoader* aScriptLoader,
+ nsIGlobalObject* aGlobalObject)
+ : ModuleLoaderBase(aScriptLoader, aGlobalObject,
+ GetCurrentSerialEventTarget()) {
+ // This should be constructed on a worklet thread.
+ MOZ_ASSERT(!NS_IsMainThread());
+}
+
+already_AddRefed<ModuleLoadRequest> WorkletModuleLoader::CreateStaticImport(
+ nsIURI* aURI, ModuleLoadRequest* aParent) {
+ const nsMainThreadPtrHandle<WorkletFetchHandler>& handlerRef =
+ aParent->GetWorkletLoadContext()->GetHandlerRef();
+ RefPtr<WorkletLoadContext> loadContext = new WorkletLoadContext(handlerRef);
+
+ // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-the-descendants-of-a-module-script
+ // Step 11. Perform the internal module script graph fetching procedure
+ //
+ // https://html.spec.whatwg.org/multipage/webappapis.html#internal-module-script-graph-fetching-procedure
+ // Step 5. Fetch a single module script with referrer is referringScript's
+ // base URL,
+ nsIURI* referrer = aParent->mURI;
+ RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest(
+ aURI, aParent->mFetchOptions, SRIMetadata(), referrer, loadContext,
+ false, /* is top level */
+ false, /* is dynamic import */
+ this, aParent->mVisitedSet, aParent->GetRootModule());
+
+ request->mURL = request->mURI->GetSpecOrDefault();
+ return request.forget();
+}
+
+already_AddRefed<ModuleLoadRequest> WorkletModuleLoader::CreateDynamicImport(
+ JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript,
+ JS::Handle<JS::Value> aReferencingPrivate, JS::Handle<JSString*> aSpecifier,
+ JS::Handle<JSObject*> aPromise) {
+ return nullptr;
+}
+
+bool WorkletModuleLoader::CanStartLoad(ModuleLoadRequest* aRequest,
+ nsresult* aRvOut) {
+ return true;
+}
+
+nsresult WorkletModuleLoader::StartFetch(ModuleLoadRequest* aRequest) {
+ InsertRequest(aRequest->mURI, aRequest);
+
+ RefPtr<StartFetchRunnable> runnable =
+ new StartFetchRunnable(aRequest->GetWorkletLoadContext()->GetHandlerRef(),
+ aRequest->mURI, aRequest->mReferrer);
+ NS_DispatchToMainThread(runnable.forget());
+ return NS_OK;
+}
+
+nsresult WorkletModuleLoader::CompileFetchedModule(
+ JSContext* aCx, JS::Handle<JSObject*> aGlobal, JS::CompileOptions& aOptions,
+ ModuleLoadRequest* aRequest, JS::MutableHandle<JSObject*> aModuleScript) {
+ RefPtr<JS::Stencil> stencil;
+ MOZ_ASSERT(aRequest->IsTextSource());
+
+ MaybeSourceText maybeSource;
+ nsresult rv = aRequest->GetScriptSource(aCx, &maybeSource);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ auto compile = [&](auto& source) {
+ return JS::CompileModuleScriptToStencil(aCx, aOptions, source);
+ };
+ stencil = maybeSource.mapNonEmpty(compile);
+
+ if (!stencil) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::InstantiateOptions instantiateOptions(aOptions);
+ aModuleScript.set(
+ JS::InstantiateModuleStencil(aCx, instantiateOptions, stencil));
+ return aModuleScript ? NS_OK : NS_ERROR_FAILURE;
+}
+
+// AddModuleResultRunnable is a Runnable which will notify the result of
+// Worklet::AddModule on the main thread.
+class AddModuleResultRunnable final : public Runnable {
+ public:
+ explicit AddModuleResultRunnable(
+ const nsMainThreadPtrHandle<WorkletFetchHandler>& aHandlerRef,
+ bool aSucceeded)
+ : Runnable("Worklet::AddModuleResultRunnable"),
+ mHandlerRef(aHandlerRef),
+ mSucceeded(aSucceeded) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ }
+
+ ~AddModuleResultRunnable() = default;
+
+ NS_IMETHOD
+ Run() override;
+
+ private:
+ nsMainThreadPtrHandle<WorkletFetchHandler> mHandlerRef;
+ bool mSucceeded;
+};
+
+NS_IMETHODIMP
+AddModuleResultRunnable::Run() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mSucceeded) {
+ mHandlerRef->ExecutionSucceeded();
+ } else {
+ mHandlerRef->ExecutionFailed();
+ }
+
+ return NS_OK;
+}
+
+class AddModuleThrowErrorRunnable final : public Runnable,
+ public StructuredCloneHolder {
+ public:
+ explicit AddModuleThrowErrorRunnable(
+ const nsMainThreadPtrHandle<WorkletFetchHandler>& aHandlerRef)
+ : Runnable("Worklet::AddModuleThrowErrorRunnable"),
+ StructuredCloneHolder(CloningSupported, TransferringNotSupported,
+ StructuredCloneScope::SameProcess),
+ mHandlerRef(aHandlerRef) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ }
+
+ ~AddModuleThrowErrorRunnable() = default;
+
+ NS_IMETHOD
+ Run() override;
+
+ private:
+ nsMainThreadPtrHandle<WorkletFetchHandler> mHandlerRef;
+};
+
+NS_IMETHODIMP
+AddModuleThrowErrorRunnable::Run() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIGlobalObject> global =
+ do_QueryInterface(mHandlerRef->mWorklet->GetParentObject());
+ MOZ_ASSERT(global);
+
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(global))) {
+ mHandlerRef->ExecutionFailed();
+ return NS_ERROR_FAILURE;
+ }
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JS::Value> error(cx);
+ ErrorResult result;
+ Read(global, cx, &error, result);
+ Unused << NS_WARN_IF(result.Failed());
+ mHandlerRef->ExecutionFailed(error);
+
+ return NS_OK;
+}
+
+void WorkletModuleLoader::OnModuleLoadComplete(ModuleLoadRequest* aRequest) {
+ if (!aRequest->IsTopLevel()) {
+ return;
+ }
+
+ const nsMainThreadPtrHandle<WorkletFetchHandler>& handlerRef =
+ aRequest->GetWorkletLoadContext()->GetHandlerRef();
+
+ auto addModuleFailed = MakeScopeExit([&] {
+ RefPtr<AddModuleResultRunnable> runnable =
+ new AddModuleResultRunnable(handlerRef, false);
+ NS_DispatchToMainThread(runnable.forget());
+ });
+
+ if (!aRequest->mModuleScript) {
+ return;
+ }
+
+ if (!aRequest->InstantiateModuleGraph()) {
+ return;
+ }
+
+ nsresult rv = aRequest->EvaluateModule();
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ bool hasError = aRequest->mModuleScript->HasErrorToRethrow();
+ if (hasError) {
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(GetGlobalObject()))) {
+ return;
+ }
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JS::Value> error(cx, aRequest->mModuleScript->ErrorToRethrow());
+ RefPtr<AddModuleThrowErrorRunnable> runnable =
+ new AddModuleThrowErrorRunnable(handlerRef);
+ ErrorResult result;
+ runnable->Write(cx, error, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return;
+ }
+
+ addModuleFailed.release();
+ NS_DispatchToMainThread(runnable.forget());
+ return;
+ }
+
+ addModuleFailed.release();
+ RefPtr<AddModuleResultRunnable> runnable =
+ new AddModuleResultRunnable(handlerRef, true);
+ NS_DispatchToMainThread(runnable.forget());
+}
+
+// TODO: Bug 1808301: Call FormatLocalizedString from a worklet thread.
+nsresult WorkletModuleLoader::GetResolveFailureMessage(
+ ResolveError aError, const nsAString& aSpecifier, nsAString& aResult) {
+ uint8_t index = static_cast<uint8_t>(aError);
+ MOZ_ASSERT(index < static_cast<uint8_t>(ResolveError::Length));
+ MOZ_ASSERT(mLocalizedStrs);
+ MOZ_ASSERT(!mLocalizedStrs->IsEmpty());
+ if (!mLocalizedStrs || NS_WARN_IF(mLocalizedStrs->IsEmpty())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const nsString& localizedStr = mLocalizedStrs->ElementAt(index);
+
+ AutoTArray<nsString, 1> params;
+ params.AppendElement(aSpecifier);
+
+ nsStringBundleBase::FormatString(localizedStr.get(), params, aResult);
+ return NS_OK;
+}
+
+void WorkletModuleLoader::InsertRequest(nsIURI* aURI,
+ ModuleLoadRequest* aRequest) {
+ mFetchingRequests.InsertOrUpdate(aURI, aRequest);
+}
+
+void WorkletModuleLoader::RemoveRequest(nsIURI* aURI) {
+ MOZ_ASSERT(mFetchingRequests.Remove(aURI));
+}
+
+ModuleLoadRequest* WorkletModuleLoader::GetRequest(nsIURI* aURI) const {
+ RefPtr<ModuleLoadRequest> req;
+ MOZ_ALWAYS_TRUE(mFetchingRequests.Get(aURI, getter_AddRefs(req)));
+ return req;
+}
+
+} // namespace mozilla::dom::loader
diff --git a/dom/worklet/loader/WorkletModuleLoader.h b/dom/worklet/loader/WorkletModuleLoader.h
new file mode 100644
index 0000000000..818720ced8
--- /dev/null
+++ b/dom/worklet/loader/WorkletModuleLoader.h
@@ -0,0 +1,119 @@
+/* -*- 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_worklet_WorkletModuleLoader_h
+#define mozilla_dom_worklet_WorkletModuleLoader_h
+
+#include "js/loader/LoadContextBase.h"
+#include "js/loader/ModuleLoaderBase.h"
+#include "js/loader/ResolveResult.h" // For ResolveError
+#include "mozilla/dom/WorkletFetchHandler.h"
+
+namespace mozilla::dom {
+namespace loader {
+class WorkletScriptLoader : public JS::loader::ScriptLoaderInterface {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(WorkletScriptLoader)
+
+ nsIURI* GetBaseURI() const override { return nullptr; }
+
+ void ReportErrorToConsole(ScriptLoadRequest* aRequest,
+ nsresult aResult) const override {}
+
+ void ReportWarningToConsole(
+ ScriptLoadRequest* aRequest, const char* aMessageName,
+ const nsTArray<nsString>& aParams) const override {}
+
+ nsresult FillCompileOptionsForRequest(
+ JSContext* cx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions,
+ JS::MutableHandle<JSScript*> aIntroductionScript) override {
+ aOptions->setIntroductionType("Worklet");
+ aOptions->setFileAndLine(aRequest->mURL.get(), 1);
+ aOptions->setIsRunOnce(true);
+ aOptions->setNoScriptRval(true);
+ return NS_OK;
+ }
+
+ private:
+ ~WorkletScriptLoader() = default;
+};
+
+class WorkletModuleLoader : public JS::loader::ModuleLoaderBase {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(WorkletModuleLoader,
+ JS::loader::ModuleLoaderBase)
+
+ WorkletModuleLoader(WorkletScriptLoader* aScriptLoader,
+ nsIGlobalObject* aGlobalObject);
+
+ void InsertRequest(nsIURI* aURI, JS::loader::ModuleLoadRequest* aRequest);
+ void RemoveRequest(nsIURI* aURI);
+ JS::loader::ModuleLoadRequest* GetRequest(nsIURI* aURI) const;
+
+ bool HasSetLocalizedStrings() const { return (bool)mLocalizedStrs; }
+ void SetLocalizedStrings(const nsTArray<nsString>* aStrings) {
+ mLocalizedStrs = aStrings;
+ }
+
+ private:
+ ~WorkletModuleLoader() = default;
+
+ already_AddRefed<JS::loader::ModuleLoadRequest> CreateStaticImport(
+ nsIURI* aURI, JS::loader::ModuleLoadRequest* aParent) override;
+
+ already_AddRefed<JS::loader::ModuleLoadRequest> CreateDynamicImport(
+ JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript,
+ JS::Handle<JS::Value> aReferencingPrivate,
+ JS::Handle<JSString*> aSpecifier,
+ JS::Handle<JSObject*> aPromise) override;
+
+ bool CanStartLoad(JS::loader::ModuleLoadRequest* aRequest,
+ nsresult* aRvOut) override;
+
+ nsresult StartFetch(JS::loader::ModuleLoadRequest* aRequest) override;
+
+ nsresult CompileFetchedModule(
+ JSContext* aCx, JS::Handle<JSObject*> aGlobal,
+ JS::CompileOptions& aOptions, JS::loader::ModuleLoadRequest* aRequest,
+ JS::MutableHandle<JSObject*> aModuleScript) override;
+
+ void OnModuleLoadComplete(JS::loader::ModuleLoadRequest* aRequest) override;
+
+ nsresult GetResolveFailureMessage(JS::loader::ResolveError aError,
+ const nsAString& aSpecifier,
+ nsAString& aResult) override;
+
+ // A hashtable to map a nsIURI(from main thread) to a ModuleLoadRequest(in
+ // worklet thread).
+ nsRefPtrHashtable<nsURIHashKey, JS::loader::ModuleLoadRequest>
+ mFetchingRequests;
+
+ // We get the localized strings on the main thread, and pass it to
+ // WorkletModuleLoader.
+ const nsTArray<nsString>* mLocalizedStrs = nullptr;
+};
+} // namespace loader
+
+class WorkletLoadContext : public JS::loader::LoadContextBase {
+ public:
+ explicit WorkletLoadContext(
+ const nsMainThreadPtrHandle<WorkletFetchHandler>& aHandlerRef)
+ : JS::loader::LoadContextBase(JS::loader::ContextKind::Worklet),
+ mHandlerRef(aHandlerRef) {}
+
+ const nsMainThreadPtrHandle<WorkletFetchHandler>& GetHandlerRef() const {
+ return mHandlerRef;
+ }
+
+ private:
+ ~WorkletLoadContext() = default;
+
+ nsMainThreadPtrHandle<WorkletFetchHandler> mHandlerRef;
+};
+} // namespace mozilla::dom
+#endif // mozilla_dom_worklet_WorkletModuleLoader_h
diff --git a/dom/worklet/loader/moz.build b/dom/worklet/loader/moz.build
new file mode 100644
index 0000000000..38707a81cc
--- /dev/null
+++ b/dom/worklet/loader/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+EXPORTS.mozilla.dom.worklet += [
+ "WorkletModuleLoader.h",
+]
+
+UNIFIED_SOURCES += [
+ "WorkletModuleLoader.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/worklet/moz.build b/dom/worklet/moz.build
new file mode 100644
index 0000000000..f08e75bd19
--- /dev/null
+++ b/dom/worklet/moz.build
@@ -0,0 +1,36 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+DIRS += ["loader"]
+
+EXPORTS.mozilla.dom += [
+ "Worklet.h",
+ "WorkletFetchHandler.h",
+ "WorkletGlobalScope.h",
+ "WorkletImpl.h",
+ "WorkletThread.h",
+]
+
+UNIFIED_SOURCES += [
+ "Worklet.cpp",
+ "WorkletFetchHandler.cpp",
+ "WorkletGlobalScope.cpp",
+ "WorkletImpl.cpp",
+ "WorkletThread.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/js/xpconnect/src",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+MOCHITEST_MANIFESTS += ["tests/mochitest.ini"]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/worklet/tests/common.js b/dom/worklet/tests/common.js
new file mode 100644
index 0000000000..df6005cfe4
--- /dev/null
+++ b/dom/worklet/tests/common.js
@@ -0,0 +1,23 @@
+window.onload = function () {
+ // We are the parent. Let's load the test.
+ if (parent == this || !location.search.includes("worklet_iframe")) {
+ SimpleTest.waitForExplicitFinish();
+
+ configureTest().then(() => {
+ var iframe = document.createElement("iframe");
+ iframe.src = location.href + "?worklet_iframe";
+ document.body.appendChild(iframe);
+ });
+
+ return;
+ }
+
+ // Here we are in the iframe.
+ window.SimpleTest = parent.SimpleTest;
+ window.is = parent.is;
+ window.isnot = parent.isnot;
+ window.ok = parent.ok;
+ window.info = parent.info;
+
+ runTestInIframe();
+};
diff --git a/dom/worklet/tests/dynamic_import.js b/dom/worklet/tests/dynamic_import.js
new file mode 100644
index 0000000000..a5196d212f
--- /dev/null
+++ b/dom/worklet/tests/dynamic_import.js
@@ -0,0 +1,7 @@
+import("./empty-worklet-script.js")
+ .then(() => {
+ console.log("Fail");
+ })
+ .catch(e => {
+ console.log(e.name + ": Success");
+ });
diff --git a/dom/worklet/tests/invalid_specifier.mjs b/dom/worklet/tests/invalid_specifier.mjs
new file mode 100644
index 0000000000..128b60ffa6
--- /dev/null
+++ b/dom/worklet/tests/invalid_specifier.mjs
@@ -0,0 +1,3 @@
+/* eslint-disable import/no-unassigned-import */
+/* eslint-disable import/no-unresolved */
+import "foo";
diff --git a/dom/worklet/tests/mochitest.ini b/dom/worklet/tests/mochitest.ini
new file mode 100644
index 0000000000..ebc85359cb
--- /dev/null
+++ b/dom/worklet/tests/mochitest.ini
@@ -0,0 +1,40 @@
+[DEFAULT]
+scheme = https
+support-files =
+ common.js
+
+[test_audioWorklet.html]
+support-files=worklet_audioWorklet.js
+[test_audioWorkletGlobalScopeRegisterProcessor.html]
+support-files=worklet_test_audioWorkletGlobalScopeRegisterProcessor.js
+[test_audioWorklet_WASM.html]
+skip-if = release_or_beta # requires dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled
+support-files=worklet_audioWorklet_WASM.js
+[test_audioWorklet_insecureContext.html]
+scheme = http
+skip-if =
+ http3
+[test_audioWorklet_options.html]
+skip-if = release_or_beta # requires dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled
+support-files=worklet_audioWorklet_options.js
+[test_basic.html]
+[test_console.html]
+support-files=worklet_console.js
+[test_dump.html]
+support-files=worklet_dump.js
+[test_dynamic_import.html]
+support-files=dynamic_import.js
+[test_exception.html]
+support-files =
+ worklet_exception.js
+ invalid_specifier.mjs
+[test_fetch_failed.html]
+support-files=specifier_with_user.mjs
+[test_import_with_cache.html]
+skip-if = verify
+support-files=server_import_with_cache.sjs
+[test_paintWorklet.html]
+skip-if = release_or_beta
+support-files=worklet_paintWorklet.js
+[test_promise.html]
+support-files=worklet_promise.js
diff --git a/dom/worklet/tests/server_import_with_cache.sjs b/dom/worklet/tests/server_import_with_cache.sjs
new file mode 100644
index 0000000000..ed34a7a72f
--- /dev/null
+++ b/dom/worklet/tests/server_import_with_cache.sjs
@@ -0,0 +1,12 @@
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/javascript", false);
+
+ var state = getState("alreadySent");
+ if (!state) {
+ setState("alreadySent", "1");
+ } else {
+ response.setStatusLine("1.1", 404, "Not Found");
+ }
+
+ response.write("42");
+}
diff --git a/dom/worklet/tests/specifier_with_user.mjs b/dom/worklet/tests/specifier_with_user.mjs
new file mode 100644
index 0000000000..3d9bdb45cf
--- /dev/null
+++ b/dom/worklet/tests/specifier_with_user.mjs
@@ -0,0 +1,3 @@
+/* eslint-disable import/no-unassigned-import */
+/* eslint-disable import/no-unresolved */
+import "http://user@example1.com/a.js";
diff --git a/dom/worklet/tests/test_audioWorklet.html b/dom/worklet/tests/test_audioWorklet.html
new file mode 100644
index 0000000000..f7f3665e2f
--- /dev/null
+++ b/dom/worklet/tests/test_audioWorklet.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for AudioWorklet</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function configureTest() {
+ const ConsoleAPIStorage = SpecialPowers.Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage);
+
+ function consoleListener() {
+ this.observe = this.observe.bind(this);
+ ConsoleAPIStorage.addLogEventListener(this.observe, SpecialPowers.wrap(document).nodePrincipal);
+ }
+
+ consoleListener.prototype = {
+ observe(aSubject) {
+ var obj = aSubject.wrappedJSObject;
+ if (obj.arguments[0] == "So far so good") {
+ ok(true, "Message received \\o/");
+
+ ConsoleAPIStorage.removeLogEventListener(this.observe);
+ SimpleTest.finish();
+ return;
+ }
+ }
+ }
+
+ var cl = new consoleListener();
+
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.audioworklet.enabled", true],
+ ["dom.worklet.enabled", true]]});
+}
+
+// This function is called into an iframe.
+function runTestInIframe() {
+ ok(window.isSecureContext, "Test should run in secure context");
+ var audioContext = new AudioContext();
+ ok(audioContext.audioWorklet instanceof AudioWorklet,
+ "AudioContext.audioWorklet should be an instance of AudioWorklet");
+ audioContext.audioWorklet.addModule("worklet_audioWorklet.js")
+}
+</script>
+</body>
+</html>
diff --git a/dom/worklet/tests/test_audioWorkletGlobalScopeRegisterProcessor.html b/dom/worklet/tests/test_audioWorkletGlobalScopeRegisterProcessor.html
new file mode 100644
index 0000000000..c26df05ad0
--- /dev/null
+++ b/dom/worklet/tests/test_audioWorkletGlobalScopeRegisterProcessor.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for AudioWorklet</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function configureTest() {
+
+ var expected_errors = [
+ "TypeError: AudioWorkletGlobalScope.registerProcessor: Argument 2 is not a constructor.",
+ "NotSupportedError: AudioWorkletGlobalScope.registerProcessor: Argument 1 should not be an empty string.",
+ "TypeError: AudioWorkletGlobalScope.registerProcessor: Argument 2 is not an object.",
+ "TypeError: AudioWorkletGlobalScope.registerProcessor: Element 0 in parameterDescriptors can't be converted to a dictionary.",
+ "NotSupportedError: AudioWorkletGlobalScope.registerProcessor: Argument 1 is invalid: a class with the same name is already registered.",
+ "TypeError: AudioWorkletGlobalScope.registerProcessor: Missing required 'name' member of AudioParamDescriptor.",
+ "TypeError: AudioWorkletGlobalScope.registerProcessor: 'defaultValue' member of AudioParamDescriptor is not a finite floating-point value.",
+ "TypeError: AudioWorkletGlobalScope.registerProcessor: 'minValue' member of AudioParamDescriptor is not a finite floating-point value.",
+ "TypeError: AudioWorkletGlobalScope.registerProcessor: 'maxValue' member of AudioParamDescriptor is not a finite floating-point value.",
+ "NotSupportedError: AudioWorkletGlobalScope.registerProcessor: Duplicated name \"test\" in parameterDescriptors.",
+ "TypeError: AudioWorkletGlobalScope.registerProcessor: Element 0 in parameterDescriptors can't be converted to a dictionary.",
+ "InvalidStateError: AudioWorkletGlobalScope.registerProcessor: In parameterDescriptors, test defaultValue is out of the range defined by minValue and maxValue.",
+ "InvalidStateError: AudioWorkletGlobalScope.registerProcessor: In parameterDescriptors, test defaultValue is out of the range defined by minValue and maxValue.",
+ "InvalidStateError: AudioWorkletGlobalScope.registerProcessor: In parameterDescriptors, test minValue should be smaller than maxValue.",
+ ];
+
+ var expected_errors_i = 0;
+
+ const ConsoleAPIStorage = SpecialPowers.Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage);
+
+ function consoleListener() {
+ this.observe = this.observe.bind(this);
+ ConsoleAPIStorage.addLogEventListener(this.observe, SpecialPowers.wrap(document).nodePrincipal);
+ }
+
+ consoleListener.prototype = {
+ observe(aSubject) {
+ var obj = aSubject.wrappedJSObject;
+ if (obj.arguments[0] == expected_errors[expected_errors_i]) {
+ ok(true, "Expected error received: " + obj.arguments[0]);
+ expected_errors_i++;
+ }
+
+ if (expected_errors_i == expected_errors.length) {
+ // All errors have been received, this test has been completed
+ // succesfully!
+ ConsoleAPIStorage.removeLogEventListener(this.observe);
+ SimpleTest.finish();
+ return;
+ }
+ }
+ }
+
+ var cl = new consoleListener();
+
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.audioworklet.enabled", true],
+ ["dom.worklet.enabled", true]]});
+}
+
+// This function is called into an iframe.
+function runTestInIframe() {
+ ok(window.isSecureContext, "Test should run in secure context");
+ var audioContext = new AudioContext();
+ ok(audioContext.audioWorklet instanceof AudioWorklet,
+ "AudioContext.audioWorklet should be an instance of AudioWorklet");
+ audioContext.audioWorklet.addModule("worklet_test_audioWorkletGlobalScopeRegisterProcessor.js")
+}
+</script>
+</body>
+</html>
diff --git a/dom/worklet/tests/test_audioWorklet_WASM.html b/dom/worklet/tests/test_audioWorklet_WASM.html
new file mode 100644
index 0000000000..127cc8b924
--- /dev/null
+++ b/dom/worklet/tests/test_audioWorklet_WASM.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for AudioWorklet + WASM</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function configureTest() {
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.audioworklet.enabled", true],
+ ["dom.worklet.enabled", true],
+ ["dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled", true],
+ ["browser.tabs.remote.useCrossOriginOpenerPolicy", true],
+ ["browser.tabs.remote.useCrossOriginEmbedderPolicy", true],
+ ["javascript.options.shared_memory", true],
+ ]});
+}
+
+function create_wasmModule() {
+ return new Promise(resolve => {
+ info("Checking if we can play with WebAssembly...");
+
+ if (!SpecialPowers.Cu.getJSTestingFunctions().wasmIsSupported()) {
+ resolve(null);
+ return;
+ }
+
+ ok(WebAssembly, "WebAssembly object should exist");
+ ok(WebAssembly.compile, "WebAssembly.compile function should exist");
+
+ const wasmTextToBinary = SpecialPowers.unwrap(SpecialPowers.Cu.getJSTestingFunctions().wasmTextToBinary);
+ /*
+ js -e '
+ t = wasmTextToBinary(`
+ (module
+ (func $foo (result i32) (i32.const 42))
+ (export "foo" (func $foo))
+ )
+ `);
+ print(t)
+ '
+ */
+ // eslint-disable-next-line
+ const fooModuleCode = new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,127,3,2,1,0,7,7,1,3,102,111,111,0,0,10,6,1,4,0,65,42,11,0,13,4,110,97,109,101,1,6,1,0,3,102,111,111]);
+
+ WebAssembly.compile(fooModuleCode).then(m => {
+ ok(m instanceof WebAssembly.Module, "The WasmModule has been compiled.");
+ resolve(m);
+ }, () => {
+ ok(false, "The compilation of the wasmModule failed.");
+ resolve(null);
+ });
+ });
+}
+
+function runTestInIframe() {
+ let audioContext = new AudioContext();
+ audioContext.audioWorklet.addModule("worklet_audioWorklet_WASM.js")
+ .then(() => create_wasmModule())
+ .then(wasmModule => {
+ const node = new AudioWorkletNode(audioContext, 'wasm');
+ let msgId = 0;
+ node.port.onmessage = e => {
+ if (msgId++ == 0) {
+ ok(e.data.wasmModule instanceof WebAssembly.Module, "WasmModule received");
+ } else {
+ ok(e.data.sab instanceof SharedArrayBuffer, "SAB received");
+ SimpleTest.finish();
+ }
+ }
+
+ node.port.postMessage({wasmModule});
+ node.port.postMessage({sab: new SharedArrayBuffer(1024)});
+ node.connect(audioContext.destination);
+ });
+}
+</script>
+
+</body>
+</html>
diff --git a/dom/worklet/tests/test_audioWorklet_insecureContext.html b/dom/worklet/tests/test_audioWorklet_insecureContext.html
new file mode 100644
index 0000000000..3cbb419ac3
--- /dev/null
+++ b/dom/worklet/tests/test_audioWorklet_insecureContext.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for No AudioWorklet in insecure context</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function configureTest() {
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.audioworklet.enabled", true],
+ ["dom.worklet.enabled", true]]});
+}
+
+// This function is called into an iframe.
+function runTestInIframe() {
+ ok(!window.isSecureContext, "Test should run in an insecure context");
+ var audioContext = new AudioContext();
+ ok(!("audioWorklet" in audioContext),
+ "AudioWorklet shouldn't be defined in AudioContext in a insecure context");
+ SimpleTest.finish();
+}
+</script>
+</body>
+</html>
diff --git a/dom/worklet/tests/test_audioWorklet_options.html b/dom/worklet/tests/test_audioWorklet_options.html
new file mode 100644
index 0000000000..df7a8b5649
--- /dev/null
+++ b/dom/worklet/tests/test_audioWorklet_options.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for AudioWorklet + Options + WASM</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+<script type="application/javascript">
+
+function configureTest() {
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.audioworklet.enabled", true],
+ ["dom.worklet.enabled", true],
+ ["dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled", true],
+ ["browser.tabs.remote.useCrossOriginOpenerPolicy", true],
+ ["browser.tabs.remote.useCrossOriginEmbedderPolicy", true],
+ ["javascript.options.shared_memory", true],
+ ]});
+}
+
+function create_wasmModule() {
+ return new Promise(resolve => {
+ info("Checking if we can play with WebAssembly...");
+
+ if (!SpecialPowers.Cu.getJSTestingFunctions().wasmIsSupported()) {
+ resolve(null);
+ return;
+ }
+
+ ok(WebAssembly, "WebAssembly object should exist");
+ ok(WebAssembly.compile, "WebAssembly.compile function should exist");
+
+ const wasmTextToBinary = SpecialPowers.unwrap(SpecialPowers.Cu.getJSTestingFunctions().wasmTextToBinary);
+
+ /*
+ js -e '
+ t = wasmTextToBinary(`
+ (module
+ (func $foo (result i32) (i32.const 42))
+ (export "foo" (func $foo))
+ )
+ `);
+ print(t)
+ '
+ */
+ // eslint-disable-next-line
+ const fooModuleCode = new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,127,3,2,1,0,7,7,1,3,102,111,111,0,0,10,6,1,4,0,65,42,11,0,13,4,110,97,109,101,1,6,1,0,3,102,111,111]);
+
+ WebAssembly.compile(fooModuleCode).then(m => {
+ ok(m instanceof WebAssembly.Module, "The WasmModule has been compiled.");
+ resolve(m);
+ }, () => {
+ ok(false, "The compilation of the wasmModule failed.");
+ resolve(null);
+ });
+ });
+}
+
+function runTestInIframe() {
+ let audioContext = new AudioContext();
+ audioContext.audioWorklet.addModule("worklet_audioWorklet_options.js")
+ .then(() => create_wasmModule())
+ .then(wasmModule => {
+ const node = new AudioWorkletNode(audioContext, 'options', { processorOptions: {
+ wasmModule, sab: new SharedArrayBuffer(1024),
+ }});
+ node.port.onmessage = e => {
+ ok(e.data.wasmModule instanceof WebAssembly.Module, "WasmModule received");
+ ok(e.data.sab instanceof SharedArrayBuffer, "SAB received");
+ SimpleTest.finish();
+ }
+
+ node.connect(audioContext.destination);
+ });
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/worklet/tests/test_basic.html b/dom/worklet/tests/test_basic.html
new file mode 100644
index 0000000000..b13cadd6d1
--- /dev/null
+++ b/dom/worklet/tests/test_basic.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Worklet</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function configureTest() {
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.audioworklet.enabled", true],
+ ["dom.worklet.enabled", true]]});
+}
+
+// This function is called into an iframe.
+function runTestInIframe() {
+ var audioContext = new AudioContext();
+ ok(!!audioContext.audioWorklet, "audioContext.audioWorklet exists");
+
+ // First loading
+ audioContext.audioWorklet.addModule("common.js")
+ .then(() => {
+ ok(true, "Import should load a resource.");
+ })
+
+ // Second loading - same file
+ .then(() => {
+ return audioContext.audioWorklet.addModule("common.js")
+ })
+ .then(() => {
+ ok(true, "Import should load a resource.");
+ })
+
+ // 3rd loading - a network error
+ .then(() => {
+ return audioContext.audioWorklet.addModule("404.js");
+ })
+ .then(() => {
+ ok(false, "The loading should fail.");
+ }, () => {
+ ok(true, "The loading should fail.");
+ })
+
+ // 4th loading - a network error
+ .then(() => {
+ return audioContext.audioWorklet.addModule("404.js");
+ })
+ .then(() => {
+ ok(false, "The loading should fail.");
+ }, () => {
+ ok(true, "The loading should fail.");
+ })
+
+ // done
+ .then(() => {
+ SimpleTest.finish();
+ });
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/worklet/tests/test_console.html b/dom/worklet/tests/test_console.html
new file mode 100644
index 0000000000..c33b93001f
--- /dev/null
+++ b/dom/worklet/tests/test_console.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Worklet - Console</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+const WORKLET_SCRIPT = "worklet_console.js";
+
+function configureTest() {
+ const ConsoleAPIStorage = SpecialPowers.Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage);
+
+ function consoleListener() {
+ this.observe = this.observe.bind(this);
+ ConsoleAPIStorage.addLogEventListener(this.observe, SpecialPowers.wrap(document).nodePrincipal);
+ }
+
+ consoleListener.prototype = {
+ observe(aSubject) {
+ var obj = aSubject.wrappedJSObject;
+ if (obj.arguments[0] == "Hello world from a worklet") {
+ ok(true, "Message received \\o/");
+ is(obj.filename,
+ new URL(WORKLET_SCRIPT, document.baseURI).toString());
+
+ ConsoleAPIStorage.removeLogEventListener(this.observe);
+ SimpleTest.finish();
+ return;
+ }
+ }
+ }
+
+ var cl = new consoleListener();
+
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.audioworklet.enabled", true],
+ ["dom.worklet.enabled", true]]});
+}
+
+// This function is called into an iframe.
+function runTestInIframe() {
+ var audioContext = new AudioContext();
+ audioContext.audioWorklet.addModule(WORKLET_SCRIPT);
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/worklet/tests/test_dump.html b/dom/worklet/tests/test_dump.html
new file mode 100644
index 0000000000..f7885a1e21
--- /dev/null
+++ b/dom/worklet/tests/test_dump.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Worklet - Console</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function configureTest() {
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.audioworklet.enabled", true],
+ ["dom.worklet.enabled", true]]});
+}
+
+// This function is called into an iframe.
+function runTestInIframe() {
+ var audioContext = new AudioContext();
+ audioContext.audioWorklet.addModule("worklet_dump.js")
+ .then(() => {
+ ok(true, "All good!");
+ SimpleTest.finish();
+ });
+}
+</script>
+</body>
+</html>
diff --git a/dom/worklet/tests/test_dynamic_import.html b/dom/worklet/tests/test_dynamic_import.html
new file mode 100644
index 0000000000..e0326b976c
--- /dev/null
+++ b/dom/worklet/tests/test_dynamic_import.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test import() should throw a TypeError for Worklets</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+const WORKLET_SCRIPT = "dynamic_import.js";
+
+function configureTest() {
+ const ConsoleAPIStorage = SpecialPowers.Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage);
+
+ // We use console API to check if a TypeError has been thrown, as worklets
+ // have limitations to post the result back to the main document:
+ // Worklets have a different global, and they don't have postMessage() APIs,
+ // and static import SimpleTest.js in worklets also don't work.
+ function consoleListener() {
+ this.observe = this.observe.bind(this);
+ ConsoleAPIStorage.addLogEventListener(this.observe, SpecialPowers.wrap(document).nodePrincipal);
+ }
+
+ consoleListener.prototype = {
+ observe(aSubject) {
+ var obj = aSubject.wrappedJSObject;
+ info("Got console message:" + obj.arguments[0]);
+ is(TypeError.name + ": Success", obj.arguments[0], "import() should throw");
+
+ ConsoleAPIStorage.removeLogEventListener(this.observe);
+ SimpleTest.finish();
+ }
+ }
+
+ var cl = new consoleListener();
+
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.audioworklet.enabled", true],
+ ["dom.worklet.enabled", true]]});
+}
+
+// This function is called into an iframe.
+function runTestInIframe() {
+ var audioContext = new AudioContext();
+ audioContext.audioWorklet.addModule(WORKLET_SCRIPT);
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/worklet/tests/test_exception.html b/dom/worklet/tests/test_exception.html
new file mode 100644
index 0000000000..598dc859f3
--- /dev/null
+++ b/dom/worklet/tests/test_exception.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Exception in Worklet script</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function configureTest() {
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.audioworklet.enabled", true],
+ ["dom.worklet.enabled", true]]});
+}
+
+// This function is called into an iframe.
+function runTestInIframe() {
+ let error;
+
+ // This loading should fail
+ var audioContext = new AudioContext();
+ audioContext.audioWorklet.addModule("404.js")
+ .then(() => {
+ ok(false, "We should not be called!");
+ }, () => {
+ ok(true, "The script thrown but we are still here.");
+ })
+
+ // This should throw from JS
+ .then(() => {
+ return audioContext.audioWorklet.addModule("worklet_exception.js")
+ })
+ .then(() => {
+ ok(true, "The script threw but we are still here.");
+ }, () => {
+ ok(false, "We should not be called!");
+ })
+
+ // invalid_specifier.mjs will throw a TypeError.
+ .then(() => {
+ return audioContext.audioWorklet.addModule("invalid_specifier.mjs")
+ })
+ .then(() => {
+ ok(false, "We should not be called!");
+ }, (e) => {
+ ok(true, "The script thrown but we are still here.");
+ ok(e instanceof TypeError, "The error should be a TypeError.");
+ error = e;
+ })
+
+ // import "invalid_specifier.mjs" again, this will reuse the response from the
+ // previous addModule("invalid_specifier.mjs") call.
+ .then(() => {
+ return audioContext.audioWorklet.addModule("invalid_specifier.mjs")
+ })
+ .then(() => {
+ ok(false, "We should not be called!");
+ }, (e) => {
+ ok(true, "The script thrown but we are still here.");
+ ok (e === error, "The TypeError object should be reused.");
+ })
+
+ .then(() => {
+ SimpleTest.finish();
+ });
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/worklet/tests/test_fetch_failed.html b/dom/worklet/tests/test_fetch_failed.html
new file mode 100644
index 0000000000..b5d316805f
--- /dev/null
+++ b/dom/worklet/tests/test_fetch_failed.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test fetch an child module script with an invalid uri for Worklet</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function configureTest() {
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.audioworklet.enabled", true],
+ ["dom.worklet.enabled", true]]});
+}
+
+// This function is called into an iframe.
+function runTestInIframe() {
+ var audioContext = new AudioContext();
+ ok(!!audioContext.audioWorklet, "audioContext.audioWorklet exists");
+
+ audioContext.audioWorklet.addModule("specifier_with_user.mjs")
+ .then(() => {
+ ok(false, "Error: load shouldn't succeed.");
+ }, () => {
+ ok(true, "OK: load should fail.");
+ })
+
+ // done
+ .then(() => {
+ SimpleTest.finish();
+ });
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/worklet/tests/test_import_with_cache.html b/dom/worklet/tests/test_import_with_cache.html
new file mode 100644
index 0000000000..de1744f9cc
--- /dev/null
+++ b/dom/worklet/tests/test_import_with_cache.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Worklet</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function configureTest() {
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.audioworklet.enabled", true],
+ ["dom.worklet.enabled", true]]});
+}
+
+// This function is called into an iframe.
+function runTestInIframe() {
+ var audioContext = new AudioContext();
+ function loading() {
+ audioContext.audioWorklet.addModule("server_import_with_cache.sjs")
+ .then(() => {
+ ok(true, "Import should load a resource.");
+ }, () => {
+ ok(false, "Import should load a resource.");
+ })
+ .then(() => {
+ done();
+ });
+ }
+
+ var count = 0;
+ const MAX = 10;
+
+ function done() {
+ if (++count == MAX) {
+ SimpleTest.finish();
+ }
+ }
+
+ for (var i = 0; i < MAX; ++i) {
+ loading();
+ }
+}
+</script>
+</body>
+</html>
diff --git a/dom/worklet/tests/test_paintWorklet.html b/dom/worklet/tests/test_paintWorklet.html
new file mode 100644
index 0000000000..a02a2757fb
--- /dev/null
+++ b/dom/worklet/tests/test_paintWorklet.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for PaintWorklet</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function configureTest() {
+ const ConsoleAPIStorage = SpecialPowers.Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage);
+
+ function consoleListener() {
+ this.observe = this.observe.bind(this);
+ ConsoleAPIStorage.addLogEventListener(this.observe, SpecialPowers.wrap(document).nodePrincipal);
+ }
+
+ consoleListener.prototype = {
+ observe(aSubject) {
+ var obj = aSubject.wrappedJSObject;
+ if (obj.arguments[0] == "So far so good") {
+ ok(true, "Message received \\o/");
+
+ ConsoleAPIStorage.removeLogEventListener(this.observe);
+ SimpleTest.finish();
+ return;
+ }
+ }
+ }
+
+ var cl = new consoleListener();
+
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.paintWorklet.enabled", true],
+ ["dom.worklet.enabled", true]]});
+}
+
+// This function is called into an iframe.
+function runTestInIframe() {
+ paintWorklet.addModule("worklet_paintWorklet.js")
+}
+</script>
+</body>
+</html>
diff --git a/dom/worklet/tests/test_promise.html b/dom/worklet/tests/test_promise.html
new file mode 100644
index 0000000000..8ea71af9a6
--- /dev/null
+++ b/dom/worklet/tests/test_promise.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for promise in worklet</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function configureTest() {
+ return SpecialPowers.pushPrefEnv(
+ {"set": [["dom.audioworklet.enabled", true],
+ ["dom.worklet.enabled", true]]});
+}
+
+function runTestInIframe() {
+ if (!SpecialPowers.Cu.getJSTestingFunctions().wasmIsSupported()) {
+ SimpleTest.finish();
+ return;
+ }
+
+ ok(window.isSecureContext, "Test should run in secure context");
+ var audioContext = new AudioContext();
+ ok(audioContext.audioWorklet instanceof AudioWorklet,
+ "AudioContext.audioWorklet should be an instance of AudioWorklet");
+ audioContext.audioWorklet.addModule("worklet_promise.js")
+ .then(() => {
+ const node = new AudioWorkletNode(audioContext, 'promise');
+ node.port.onmessage = e => {
+ ok(e.data instanceof WebAssembly.Module, "The WasmModule has been compiled into the worklet.");
+ SimpleTest.finish();
+ }
+
+ const wasmTextToBinary = SpecialPowers.unwrap(SpecialPowers.Cu.getJSTestingFunctions().wasmTextToBinary);
+ /*
+ js -e '
+ t = wasmTextToBinary(`
+ (module
+ (func $foo (result i32) (i32.const 42))
+ (export "foo" (func $foo))
+ )
+ `);
+ print(t)
+ '
+ */
+ // eslint-disable-next-line
+ const fooModuleCode = new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,127,3,2,1,0,7,7,1,3,102,111,111,0,0,10,6,1,4,0,65,42,11,0,13,4,110,97,109,101,1,6,1,0,3,102,111,111]);
+
+ node.port.postMessage(fooModuleCode);
+ });
+}
+</script>
+</body>
+</html>
diff --git a/dom/worklet/tests/worklet_audioWorklet.js b/dom/worklet/tests/worklet_audioWorklet.js
new file mode 100644
index 0000000000..fa916d4359
--- /dev/null
+++ b/dom/worklet/tests/worklet_audioWorklet.js
@@ -0,0 +1,16 @@
+class DummyProcessWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+
+ process() {
+ // Do nothing, output silence
+ }
+}
+
+// We need to pass a valid AudioWorkletProcessor here, otherwise, it will fail,
+// and the console.log won't be executed
+registerProcessor("sure!", DummyProcessWorkletProcessor);
+console.log(
+ globalThis instanceof AudioWorkletGlobalScope ? "So far so good" : "error"
+);
diff --git a/dom/worklet/tests/worklet_audioWorklet_WASM.js b/dom/worklet/tests/worklet_audioWorklet_WASM.js
new file mode 100644
index 0000000000..1215b4c8d0
--- /dev/null
+++ b/dom/worklet/tests/worklet_audioWorklet_WASM.js
@@ -0,0 +1,16 @@
+class WasmProcessWorkletProcessor extends AudioWorkletProcessor {
+ constructor(...args) {
+ super(...args);
+ this.port.onmessage = e => {
+ // Let's send it back.
+ this.port.postMessage(e.data);
+ };
+ }
+
+ process(inputs, outputs, parameters) {
+ // Do nothing, output silence
+ return true;
+ }
+}
+
+registerProcessor("wasm", WasmProcessWorkletProcessor);
diff --git a/dom/worklet/tests/worklet_audioWorklet_options.js b/dom/worklet/tests/worklet_audioWorklet_options.js
new file mode 100644
index 0000000000..eb7a704234
--- /dev/null
+++ b/dom/worklet/tests/worklet_audioWorklet_options.js
@@ -0,0 +1,12 @@
+class OptionsProcessWorkletProcessor extends AudioWorkletProcessor {
+ constructor(...args) {
+ super(...args);
+ this.port.postMessage(args[0].processorOptions);
+ }
+
+ process(inputs, outputs, parameters) {
+ return true;
+ }
+}
+
+registerProcessor("options", OptionsProcessWorkletProcessor);
diff --git a/dom/worklet/tests/worklet_console.js b/dom/worklet/tests/worklet_console.js
new file mode 100644
index 0000000000..557beb1af2
--- /dev/null
+++ b/dom/worklet/tests/worklet_console.js
@@ -0,0 +1 @@
+console.log("Hello world from a worklet");
diff --git a/dom/worklet/tests/worklet_dump.js b/dom/worklet/tests/worklet_dump.js
new file mode 100644
index 0000000000..439d13f700
--- /dev/null
+++ b/dom/worklet/tests/worklet_dump.js
@@ -0,0 +1 @@
+dump("Hello world from a worklet");
diff --git a/dom/worklet/tests/worklet_exception.js b/dom/worklet/tests/worklet_exception.js
new file mode 100644
index 0000000000..f3b473756e
--- /dev/null
+++ b/dom/worklet/tests/worklet_exception.js
@@ -0,0 +1 @@
+foobar();
diff --git a/dom/worklet/tests/worklet_paintWorklet.js b/dom/worklet/tests/worklet_paintWorklet.js
new file mode 100644
index 0000000000..7cf5256e51
--- /dev/null
+++ b/dom/worklet/tests/worklet_paintWorklet.js
@@ -0,0 +1,5 @@
+// This should work for real... at some point.
+registerPaint("sure!", () => {});
+console.log(
+ globalThis instanceof PaintWorkletGlobalScope ? "So far so good" : "error"
+);
diff --git a/dom/worklet/tests/worklet_promise.js b/dom/worklet/tests/worklet_promise.js
new file mode 100644
index 0000000000..8c593fd001
--- /dev/null
+++ b/dom/worklet/tests/worklet_promise.js
@@ -0,0 +1,22 @@
+class WasmProcessWorkletProcessor extends AudioWorkletProcessor {
+ constructor(...args) {
+ super(...args);
+ this.port.onmessage = e => {
+ WebAssembly.compile(e.data).then(
+ m => {
+ this.port.postMessage(m);
+ },
+ () => {
+ this.port.postMessage("error");
+ }
+ );
+ };
+ }
+
+ process(inputs, outputs, parameters) {
+ // Do nothing, output silence
+ return true;
+ }
+}
+
+registerProcessor("promise", WasmProcessWorkletProcessor);
diff --git a/dom/worklet/tests/worklet_test_audioWorkletGlobalScopeRegisterProcessor.js b/dom/worklet/tests/worklet_test_audioWorkletGlobalScopeRegisterProcessor.js
new file mode 100644
index 0000000000..cddb9524ec
--- /dev/null
+++ b/dom/worklet/tests/worklet_test_audioWorkletGlobalScopeRegisterProcessor.js
@@ -0,0 +1,384 @@
+// Define several classes.
+class EmptyWorkletProcessor extends AudioWorkletProcessor {}
+
+class NoProcessWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+}
+
+class BadDescriptorsWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+
+ process() {
+ // Do nothing, output silence
+ }
+
+ static get parameterDescriptors() {
+ return "A string";
+ }
+}
+
+class GoodDescriptorsWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+
+ process() {
+ // Do nothing, output silence
+ }
+
+ static get parameterDescriptors() {
+ return [
+ {
+ name: "myParam",
+ defaultValue: 0.707,
+ },
+ ];
+ }
+}
+
+class DummyProcessWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+
+ process() {
+ // Do nothing, output silence
+ }
+}
+
+class DescriptorsNoNameWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+
+ process() {
+ // Do nothing, output silence
+ }
+
+ static get parameterDescriptors() {
+ return [
+ {
+ defaultValue: 0.707,
+ },
+ ];
+ }
+}
+
+class DescriptorsDefaultValueNotNumberWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+
+ process() {
+ // Do nothing, output silence
+ }
+
+ static get parameterDescriptors() {
+ return [
+ {
+ name: "test",
+ defaultValue: "test",
+ },
+ ];
+ }
+}
+
+class DescriptorsMinValueNotNumberWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+
+ process() {
+ // Do nothing, output silence
+ }
+
+ static get parameterDescriptors() {
+ return [
+ {
+ name: "test",
+ minValue: "test",
+ },
+ ];
+ }
+}
+
+class DescriptorsMaxValueNotNumberWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+
+ process() {
+ // Do nothing, output silence
+ }
+
+ static get parameterDescriptors() {
+ return [
+ {
+ name: "test",
+ maxValue: "test",
+ },
+ ];
+ }
+}
+
+class DescriptorsDuplicatedNameWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+
+ process() {
+ // Do nothing, output silence
+ }
+
+ static get parameterDescriptors() {
+ return [
+ {
+ name: "test",
+ },
+ {
+ name: "test",
+ },
+ ];
+ }
+}
+
+class DescriptorsNotDictWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+
+ process() {
+ // Do nothing, output silence
+ }
+
+ static get parameterDescriptors() {
+ return [42];
+ }
+}
+
+class DescriptorsOutOfRangeMinWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+
+ process() {
+ // Do nothing, output silence
+ }
+
+ static get parameterDescriptors() {
+ return [
+ {
+ name: "test",
+ defaultValue: 0,
+ minValue: 1,
+ maxValue: 2,
+ },
+ ];
+ }
+}
+
+class DescriptorsOutOfRangeMaxWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+
+ process() {
+ // Do nothing, output silence
+ }
+
+ static get parameterDescriptors() {
+ return [
+ {
+ name: "test",
+ defaultValue: 3,
+ minValue: 1,
+ maxValue: 2,
+ },
+ ];
+ }
+}
+
+class DescriptorsBadRangeMaxWorkletProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ }
+
+ process() {
+ // Do nothing, output silence
+ }
+
+ static get parameterDescriptors() {
+ return [
+ {
+ name: "test",
+ defaultValue: 1.5,
+ minValue: 2,
+ maxValue: 1,
+ },
+ ];
+ }
+}
+
+// Test not a constructor
+// "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor is not a constructor."
+try {
+ registerProcessor("sure!", () => {});
+} catch (e) {
+ console.log(e);
+}
+
+// Test empty name
+// "NotSupportedError: Argument 1 of AudioWorkletGlobalScope.registerProcessor should not be an empty string."
+try {
+ registerProcessor("", EmptyWorkletProcessor);
+} catch (e) {
+ console.log(e);
+}
+
+// Test not an object
+// "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor is not an object."
+try {
+ registerProcessor("my-worklet-processor", "");
+} catch (e) {
+ console.log(e);
+}
+
+// Test Empty class definition
+registerProcessor("empty-worklet-processor", EmptyWorkletProcessor);
+
+// Test class with constructor but not process function
+registerProcessor("no-worklet-processor", NoProcessWorkletProcessor);
+
+// Test class with parameterDescriptors being iterable, but the elements are not
+// dictionaries.
+// "TypeError: AudioWorkletGlobalScope.registerProcessor: Element 0 in parameterDescriptors can't be converted to a dictionary.",
+try {
+ registerProcessor(
+ "bad-descriptors-worklet-processor",
+ BadDescriptorsWorkletProcessor
+ );
+} catch (e) {
+ console.log(e);
+}
+
+// Test class with good parameterDescriptors
+// No error expected here
+registerProcessor(
+ "good-descriptors-worklet-processor",
+ GoodDescriptorsWorkletProcessor
+);
+
+// Test class with constructor and process function
+// No error expected here
+registerProcessor("dummy-worklet-processor", DummyProcessWorkletProcessor);
+
+// Test class adding class with the same name twice
+// "NotSupportedError: Operation is not supported: Argument 1 of AudioWorkletGlobalScope.registerProcessor is invalid: a class with the same name is already registered."
+try {
+ registerProcessor("dummy-worklet-processor", DummyProcessWorkletProcessor);
+} catch (e) {
+ console.log(e);
+}
+
+// "name" is a mandatory field in descriptors
+// "TypeError: Missing required 'name' member of AudioParamDescriptor."
+try {
+ registerProcessor(
+ "descriptors-no-name-worklet-processor",
+ DescriptorsNoNameWorkletProcessor
+ );
+} catch (e) {
+ console.log(e);
+}
+
+// "defaultValue" should be a number
+// "TypeError: 'defaultValue' member of AudioParamDescriptor is not a finite floating-point value."
+try {
+ registerProcessor(
+ "descriptors-default-value-not-number-worklet-processor",
+ DescriptorsDefaultValueNotNumberWorkletProcessor
+ );
+} catch (e) {
+ console.log(e);
+}
+
+// "min" should be a number
+// "TypeError: 'minValue' member of AudioParamDescriptor is not a finite floating-point value."
+try {
+ registerProcessor(
+ "descriptors-min-value-not-number-worklet-processor",
+ DescriptorsMinValueNotNumberWorkletProcessor
+ );
+} catch (e) {
+ console.log(e);
+}
+
+// "max" should be a number
+// "TypeError: 'maxValue' member of AudioParamDescriptor is not a finite floating-point value."
+try {
+ registerProcessor(
+ "descriptors-max-value-not-number-worklet-processor",
+ DescriptorsMaxValueNotNumberWorkletProcessor
+ );
+} catch (e) {
+ console.log(e);
+}
+
+// Duplicated values are not allowed for "name"
+// "NotSupportedError: Duplicated name \"test\" in parameterDescriptors"
+try {
+ registerProcessor(
+ "descriptors-duplicated-name-worklet-processor",
+ DescriptorsDuplicatedNameWorkletProcessor
+ );
+} catch (e) {
+ console.log(e);
+}
+
+// Descriptors' elements should be dictionnary
+// "TypeError: Element 0 in parameterDescriptors can't be converted to a dictionary.",
+try {
+ registerProcessor(
+ "descriptors-not-dict-worklet-processor",
+ DescriptorsNotDictWorkletProcessor
+ );
+} catch (e) {
+ console.log(e);
+}
+
+// defaultValue value should be in range [minValue, maxValue]. defaultValue < minValue is not allowed
+// "NotSupportedError: In parameterDescriptors, test defaultValue is out of the range defined by minValue and maxValue.",
+try {
+ registerProcessor(
+ "descriptors-out-of-range-min-worklet-processor",
+ DescriptorsOutOfRangeMinWorkletProcessor
+ );
+} catch (e) {
+ console.log(e);
+}
+
+// defaultValue value should be in range [minValue, maxValue]. defaultValue > maxValue is not allowed
+// "NotSupportedError: In parameterDescriptors, test defaultValue is out of the range defined by minValue and maxValue.",
+try {
+ registerProcessor(
+ "descriptors-out-of-range-max-worklet-processor",
+ DescriptorsOutOfRangeMaxWorkletProcessor
+ );
+} catch (e) {
+ console.log(e);
+}
+
+// We should have minValue < maxValue to define a valid range
+// "NotSupportedError: In parameterDescriptors, test minValue should be smaller than maxValue.",
+try {
+ registerProcessor(
+ "descriptors-bad-range-max-worklet-processor",
+ DescriptorsBadRangeMaxWorkletProcessor
+ );
+} catch (e) {
+ console.log(e);
+}