diff --git a/docs/README.md b/docs/README.md
index 65fa5815e753..98ae1ad9a79a 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -127,6 +127,7 @@ These individual tutorials expand on topics discussed in the guide above.
* [pushNotifications](api/push-notifications.md)
* [safeStorage](api/safe-storage.md)
* [screen](api/screen.md)
+* [ServiceWorkerMain](api/service-worker-main.md)
* [session](api/session.md)
* [ShareMenu](api/share-menu.md)
* [systemPreferences](api/system-preferences.md)
diff --git a/docs/api/service-worker-main.md b/docs/api/service-worker-main.md
new file mode 100644
index 000000000000..a1c3889661ca
--- /dev/null
+++ b/docs/api/service-worker-main.md
@@ -0,0 +1,34 @@
+# ServiceWorkerMain
+
+> An instance of a Service Worker representing a version of a script for a given scope.
+
+Process: [Main](../glossary.md#main-process)
+
+## Class: ServiceWorkerMain
+
+Process: [Main](../glossary.md#main-process)
+_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._
+
+### Instance Methods
+
+#### `serviceWorker.isDestroyed()` _Experimental_
+
+Returns `boolean` - Whether the service worker has been destroyed.
+
+#### `serviceWorker.startTask()` _Experimental_
+
+Returns `Object`:
+
+- `end` Function - Method to call when the task has ended. If never called, the service won't terminate while otherwise idle.
+
+Initiate a task to keep the service worker alive until ended.
+
+### Instance Properties
+
+#### `serviceWorker.scope` _Readonly_ _Experimental_
+
+A `string` representing the scope URL of the service worker.
+
+#### `serviceWorker.versionId` _Readonly_ _Experimental_
+
+A `number` representing the ID of the specific version of the service worker script in its scope.
diff --git a/docs/api/service-workers.md b/docs/api/service-workers.md
index f1fa64f62560..0d3796a93687 100644
--- a/docs/api/service-workers.md
+++ b/docs/api/service-workers.md
@@ -56,6 +56,17 @@ Returns:
Emitted when a service worker has been registered. Can occur after a call to [`navigator.serviceWorker.register('/sw.js')`](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register) successfully resolves or when a Chrome extension is loaded.
+#### Event: 'running-status-changed' _Experimental_
+
+Returns:
+
+* `details` Event\<\>
+ * `versionId` number - ID of the updated service worker version
+ * `runningStatus` string - Running status.
+ Possible values include `starting`, `running`, `stopping`, or `stopped`.
+
+Emitted when a service worker's running status has changed.
+
### Instance Methods
The following methods are available on instances of `ServiceWorkers`:
@@ -64,10 +75,56 @@ The following methods are available on instances of `ServiceWorkers`:
Returns `Record` - A [ServiceWorkerInfo](structures/service-worker-info.md) object where the keys are the service worker version ID and the values are the information about that service worker.
-#### `serviceWorkers.getFromVersionID(versionId)`
+#### `serviceWorkers.getInfoFromVersionID(versionId)`
-* `versionId` number
+* `versionId` number - ID of the service worker version
Returns [`ServiceWorkerInfo`](structures/service-worker-info.md) - Information about this service worker
If the service worker does not exist or is not running this method will throw an exception.
+
+#### `serviceWorkers.getFromVersionID(versionId)` _Deprecated_
+
+* `versionId` number - ID of the service worker version
+
+Returns [`ServiceWorkerInfo`](structures/service-worker-info.md) - Information about this service worker
+
+If the service worker does not exist or is not running this method will throw an exception.
+
+**Deprecated:** Use the new `serviceWorkers.getInfoFromVersionID` API.
+
+#### `serviceWorkers.getWorkerFromVersionID(versionId)` _Experimental_
+
+* `versionId` number - ID of the service worker version
+
+Returns [`ServiceWorkerMain | undefined`](service-worker-main.md) - Instance of the service worker associated with the given version ID. If there's no associated version, or its running status has changed to 'stopped', this will return `undefined`.
+
+#### `serviceWorkers.startWorkerForScope(scope)` _Experimental_
+
+* `scope` string - The scope of the service worker to start.
+
+Returns `Promise` - Resolves with the service worker when it's started.
+
+Starts the service worker or does nothing if already running.
+
+
+
+```js
+const { app, session } = require('electron')
+const { serviceWorkers } = session.defaultSession
+
+// Collect service workers scopes
+const workerScopes = Object.values(serviceWorkers.getAllRunning()).map((info) => info.scope)
+
+app.on('browser-window-created', async (event, window) => {
+ for (const scope of workerScopes) {
+ try {
+ // Ensure worker is started
+ await serviceWorkers.startWorkerForScope(scope)
+ } catch (error) {
+ console.error(`Failed to start service worker for ${scope}`)
+ console.error(error)
+ }
+ }
+})
+```
diff --git a/docs/api/structures/service-worker-info.md b/docs/api/structures/service-worker-info.md
index 37dd691d9624..c1192a6ccdce 100644
--- a/docs/api/structures/service-worker-info.md
+++ b/docs/api/structures/service-worker-info.md
@@ -3,3 +3,4 @@
* `scriptUrl` string - The full URL to the script that this service worker runs
* `scope` string - The base URL that this service worker is active for.
* `renderProcessId` number - The virtual ID of the process that this service worker is running in. This is not an OS level PID. This aligns with the ID set used for `webContents.getProcessId()`.
+* `versionId` number - ID of the service worker version
diff --git a/docs/breaking-changes.md b/docs/breaking-changes.md
index 82c11f63e3be..f25d46ce05cb 100644
--- a/docs/breaking-changes.md
+++ b/docs/breaking-changes.md
@@ -21,7 +21,7 @@ replacement for the deprecated methods. These new APIs allow third-party librari
preload scripts without replacing existing scripts. Also, the new `type` option allows for
additional preload targets beyond `frame`.
-```ts
+```js
// Deprecated
session.setPreloads([path.join(__dirname, 'preload.js')])
@@ -33,6 +33,21 @@ session.registerPreloadScript({
})
```
+### Deprecated: `getFromVersionID` on `session.serviceWorkers`
+
+The `session.serviceWorkers.fromVersionID(versionId)` API has been deprecated
+in favor of `session.serviceWorkers.getInfoFromVersionID(versionId)`. This was
+changed to make it more clear which object is returned with the introduction
+of the `session.serviceWorkers.getWorkerFromVersionID(versionId)` API.
+
+```js
+// Deprecated
+session.serviceWorkers.fromVersionID(versionId)
+
+// Replace with
+session.serviceWorkers.getInfoFromVersionID(versionId)
+```
+
### Deprecated: `level`, `message`, `line`, and `sourceId` arguments in `console-message` event on `WebContents`
The `console-message` event on `WebContents` has been updated to provide details on the `Event`
@@ -75,15 +90,15 @@ immediately upon being received. Otherwise, it's not guaranteed to point to the
same webpage as when received. To avoid misaligned expectations, Electron will
return `null` in the case of late access where the webpage has changed.
-```ts
+```js
ipcMain.on('unload-event', (event) => {
- event.senderFrame; // ✅ accessed immediately
-});
+ event.senderFrame // ✅ accessed immediately
+})
ipcMain.on('unload-event', async (event) => {
- await crossOriginNavigationPromise;
- event.senderFrame; // ❌ returns `null` due to late access
-});
+ await crossOriginNavigationPromise
+ event.senderFrame // ❌ returns `null` due to late access
+})
```
### Behavior Changed: custom protocol URL handling on Windows
diff --git a/filenames.auto.gni b/filenames.auto.gni
index cfc4e6484b00..388e3b3db87e 100644
--- a/filenames.auto.gni
+++ b/filenames.auto.gni
@@ -45,6 +45,7 @@ auto_filenames = {
"docs/api/push-notifications.md",
"docs/api/safe-storage.md",
"docs/api/screen.md",
+ "docs/api/service-worker-main.md",
"docs/api/service-workers.md",
"docs/api/session.md",
"docs/api/share-menu.md",
@@ -243,6 +244,7 @@ auto_filenames = {
"lib/browser/api/push-notifications.ts",
"lib/browser/api/safe-storage.ts",
"lib/browser/api/screen.ts",
+ "lib/browser/api/service-worker-main.ts",
"lib/browser/api/session.ts",
"lib/browser/api/share-menu.ts",
"lib/browser/api/system-preferences.ts",
diff --git a/filenames.gni b/filenames.gni
index 8dd8b4d346e9..b2a13956fdd2 100644
--- a/filenames.gni
+++ b/filenames.gni
@@ -296,6 +296,8 @@ filenames = {
"shell/browser/api/electron_api_screen.h",
"shell/browser/api/electron_api_service_worker_context.cc",
"shell/browser/api/electron_api_service_worker_context.h",
+ "shell/browser/api/electron_api_service_worker_main.cc",
+ "shell/browser/api/electron_api_service_worker_main.h",
"shell/browser/api/electron_api_session.cc",
"shell/browser/api/electron_api_session.h",
"shell/browser/api/electron_api_system_preferences.cc",
@@ -614,6 +616,8 @@ filenames = {
"shell/common/gin_converters/osr_converter.cc",
"shell/common/gin_converters/osr_converter.h",
"shell/common/gin_converters/serial_port_info_converter.h",
+ "shell/common/gin_converters/service_worker_converter.cc",
+ "shell/common/gin_converters/service_worker_converter.h",
"shell/common/gin_converters/std_converter.h",
"shell/common/gin_converters/time_converter.cc",
"shell/common/gin_converters/time_converter.h",
diff --git a/lib/browser/api/module-list.ts b/lib/browser/api/module-list.ts
index 98831ed64a8a..81547d159a60 100644
--- a/lib/browser/api/module-list.ts
+++ b/lib/browser/api/module-list.ts
@@ -29,6 +29,7 @@ export const browserModuleList: ElectronInternal.ModuleEntry[] = [
{ name: 'protocol', loader: () => require('./protocol') },
{ name: 'safeStorage', loader: () => require('./safe-storage') },
{ name: 'screen', loader: () => require('./screen') },
+ { name: 'ServiceWorkerMain', loader: () => require('./service-worker-main') },
{ name: 'session', loader: () => require('./session') },
{ name: 'ShareMenu', loader: () => require('./share-menu') },
{ name: 'systemPreferences', loader: () => require('./system-preferences') },
diff --git a/lib/browser/api/service-worker-main.ts b/lib/browser/api/service-worker-main.ts
new file mode 100644
index 000000000000..60d2f1e7d7f1
--- /dev/null
+++ b/lib/browser/api/service-worker-main.ts
@@ -0,0 +1,17 @@
+const { ServiceWorkerMain } = process._linkedBinding('electron_browser_service_worker_main');
+
+ServiceWorkerMain.prototype.startTask = function () {
+ // TODO(samuelmaddock): maybe make timeout configurable in the future
+ const hasTimeout = false;
+ const { id, ok } = this._startExternalRequest(hasTimeout);
+
+ if (!ok) {
+ throw new Error('Unable to start service worker task.');
+ }
+
+ return {
+ end: () => this._finishExternalRequest(id)
+ };
+};
+
+module.exports = ServiceWorkerMain;
diff --git a/lib/browser/init.ts b/lib/browser/init.ts
index 50327911aacf..3f50212555db 100644
--- a/lib/browser/init.ts
+++ b/lib/browser/init.ts
@@ -146,6 +146,9 @@ require('@electron/internal/browser/devtools');
// Load protocol module to ensure it is populated on app ready
require('@electron/internal/browser/api/protocol');
+// Load service-worker-main module to ensure it is populated on app ready
+require('@electron/internal/browser/api/service-worker-main');
+
// Load web-contents module to ensure it is populated on app ready
require('@electron/internal/browser/api/web-contents');
diff --git a/shell/browser/api/electron_api_service_worker_context.cc b/shell/browser/api/electron_api_service_worker_context.cc
index 951cf159702c..81ba67edbeb9 100644
--- a/shell/browser/api/electron_api_service_worker_context.cc
+++ b/shell/browser/api/electron_api_service_worker_context.cc
@@ -13,11 +13,18 @@
#include "gin/data_object_builder.h"
#include "gin/handle.h"
#include "gin/object_template_builder.h"
+#include "shell/browser/api/electron_api_service_worker_main.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/gurl_converter.h"
+#include "shell/common/gin_converters/service_worker_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
+#include "shell/common/gin_helper/promise.h"
+#include "shell/common/node_util.h"
+
+using ServiceWorkerStatus =
+ content::ServiceWorkerRunningInfo::ServiceWorkerVersionStatus;
namespace electron::api {
@@ -72,8 +79,8 @@ gin::WrapperInfo ServiceWorkerContext::kWrapperInfo = {gin::kEmbedderNativeGin};
ServiceWorkerContext::ServiceWorkerContext(
v8::Isolate* isolate,
ElectronBrowserContext* browser_context) {
- service_worker_context_ =
- browser_context->GetDefaultStoragePartition()->GetServiceWorkerContext();
+ storage_partition_ = browser_context->GetDefaultStoragePartition();
+ service_worker_context_ = storage_partition_->GetServiceWorkerContext();
service_worker_context_->AddObserver(this);
}
@@ -81,6 +88,23 @@ ServiceWorkerContext::~ServiceWorkerContext() {
service_worker_context_->RemoveObserver(this);
}
+void ServiceWorkerContext::OnRunningStatusChanged(
+ int64_t version_id,
+ blink::EmbeddedWorkerStatus running_status) {
+ ServiceWorkerMain* worker =
+ ServiceWorkerMain::FromVersionID(version_id, storage_partition_);
+ if (worker)
+ worker->OnRunningStatusChanged(running_status);
+
+ v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+ v8::HandleScope scope(isolate);
+ EmitWithoutEvent("running-status-changed",
+ gin::DataObjectBuilder(isolate)
+ .Set("versionId", version_id)
+ .Set("runningStatus", running_status)
+ .Build());
+}
+
void ServiceWorkerContext::OnReportConsoleMessage(
int64_t version_id,
const GURL& scope,
@@ -105,6 +129,32 @@ void ServiceWorkerContext::OnRegistrationCompleted(const GURL& scope) {
gin::DataObjectBuilder(isolate).Set("scope", scope).Build());
}
+void ServiceWorkerContext::OnVersionRedundant(int64_t version_id,
+ const GURL& scope) {
+ ServiceWorkerMain* worker =
+ ServiceWorkerMain::FromVersionID(version_id, storage_partition_);
+ if (worker)
+ worker->OnVersionRedundant();
+}
+
+void ServiceWorkerContext::OnVersionStartingRunning(int64_t version_id) {
+ OnRunningStatusChanged(version_id, blink::EmbeddedWorkerStatus::kStarting);
+}
+
+void ServiceWorkerContext::OnVersionStartedRunning(
+ int64_t version_id,
+ const content::ServiceWorkerRunningInfo& running_info) {
+ OnRunningStatusChanged(version_id, blink::EmbeddedWorkerStatus::kRunning);
+}
+
+void ServiceWorkerContext::OnVersionStoppingRunning(int64_t version_id) {
+ OnRunningStatusChanged(version_id, blink::EmbeddedWorkerStatus::kStopping);
+}
+
+void ServiceWorkerContext::OnVersionStoppedRunning(int64_t version_id) {
+ OnRunningStatusChanged(version_id, blink::EmbeddedWorkerStatus::kStopped);
+}
+
void ServiceWorkerContext::OnDestruct(content::ServiceWorkerContext* context) {
if (context == service_worker_context_) {
delete this;
@@ -124,7 +174,7 @@ v8::Local ServiceWorkerContext::GetAllRunningWorkerInfo(
return builder.Build();
}
-v8::Local ServiceWorkerContext::GetWorkerInfoFromID(
+v8::Local ServiceWorkerContext::GetInfoFromVersionID(
gin_helper::ErrorThrower thrower,
int64_t version_id) {
const base::flat_map& info_map =
@@ -138,6 +188,87 @@ v8::Local ServiceWorkerContext::GetWorkerInfoFromID(
std::move(iter->second));
}
+v8::Local ServiceWorkerContext::GetFromVersionID(
+ gin_helper::ErrorThrower thrower,
+ int64_t version_id) {
+ util::EmitWarning(thrower.isolate(),
+ "The session.serviceWorkers.getFromVersionID API is "
+ "deprecated, use "
+ "session.serviceWorkers.getInfoFromVersionID instead.",
+ "ServiceWorkersDeprecateGetFromVersionID");
+
+ return GetInfoFromVersionID(thrower, version_id);
+}
+
+v8::Local ServiceWorkerContext::GetWorkerFromVersionID(
+ v8::Isolate* isolate,
+ int64_t version_id) {
+ return ServiceWorkerMain::From(isolate, service_worker_context_,
+ storage_partition_, version_id)
+ .ToV8();
+}
+
+gin::Handle
+ServiceWorkerContext::GetWorkerFromVersionIDIfExists(v8::Isolate* isolate,
+ int64_t version_id) {
+ ServiceWorkerMain* worker =
+ ServiceWorkerMain::FromVersionID(version_id, storage_partition_);
+ if (!worker)
+ return gin::Handle();
+ return gin::CreateHandle(isolate, worker);
+}
+
+v8::Local ServiceWorkerContext::StartWorkerForScope(
+ v8::Isolate* isolate,
+ GURL scope) {
+ auto shared_promise =
+ std::make_shared>>(isolate);
+ v8::Local handle = shared_promise->GetHandle();
+
+ blink::StorageKey storage_key =
+ blink::StorageKey::CreateFirstParty(url::Origin::Create(scope));
+ service_worker_context_->StartWorkerForScope(
+ scope, storage_key,
+ base::BindOnce(&ServiceWorkerContext::DidStartWorkerForScope,
+ weak_ptr_factory_.GetWeakPtr(), shared_promise),
+ base::BindOnce(&ServiceWorkerContext::DidFailToStartWorkerForScope,
+ weak_ptr_factory_.GetWeakPtr(), shared_promise));
+
+ return handle;
+}
+
+void ServiceWorkerContext::DidStartWorkerForScope(
+ std::shared_ptr>> shared_promise,
+ int64_t version_id,
+ int process_id,
+ int thread_id) {
+ v8::Isolate* isolate = shared_promise->isolate();
+ v8::HandleScope handle_scope(isolate);
+ v8::Local service_worker_main =
+ GetWorkerFromVersionID(isolate, version_id);
+ shared_promise->Resolve(service_worker_main);
+ shared_promise.reset();
+}
+
+void ServiceWorkerContext::DidFailToStartWorkerForScope(
+ std::shared_ptr>> shared_promise,
+ content::StatusCodeResponse status) {
+ shared_promise->RejectWithErrorMessage("Failed to start service worker.");
+ shared_promise.reset();
+}
+
+v8::Local ServiceWorkerContext::StopAllWorkers(
+ v8::Isolate* isolate) {
+ auto promise = gin_helper::Promise(isolate);
+ v8::Local handle = promise.GetHandle();
+
+ service_worker_context_->StopAllServiceWorkers(base::BindOnce(
+ [](gin_helper::Promise promise) { promise.Resolve(); },
+ std::move(promise)));
+
+ return handle;
+}
+
// static
gin::Handle ServiceWorkerContext::Create(
v8::Isolate* isolate,
@@ -153,8 +284,16 @@ gin::ObjectTemplateBuilder ServiceWorkerContext::GetObjectTemplateBuilder(
ServiceWorkerContext>::GetObjectTemplateBuilder(isolate)
.SetMethod("getAllRunning",
&ServiceWorkerContext::GetAllRunningWorkerInfo)
- .SetMethod("getFromVersionID",
- &ServiceWorkerContext::GetWorkerInfoFromID);
+ .SetMethod("getFromVersionID", &ServiceWorkerContext::GetFromVersionID)
+ .SetMethod("getInfoFromVersionID",
+ &ServiceWorkerContext::GetInfoFromVersionID)
+ .SetMethod("getWorkerFromVersionID",
+ &ServiceWorkerContext::GetWorkerFromVersionID)
+ .SetMethod("_getWorkerFromVersionIDIfExists",
+ &ServiceWorkerContext::GetWorkerFromVersionIDIfExists)
+ .SetMethod("startWorkerForScope",
+ &ServiceWorkerContext::StartWorkerForScope)
+ .SetMethod("_stopAllWorkers", &ServiceWorkerContext::StopAllWorkers);
}
const char* ServiceWorkerContext::GetTypeName() {
diff --git a/shell/browser/api/electron_api_service_worker_context.h b/shell/browser/api/electron_api_service_worker_context.h
index 286c3bcecc55..9e3669e22d3b 100644
--- a/shell/browser/api/electron_api_service_worker_context.h
+++ b/shell/browser/api/electron_api_service_worker_context.h
@@ -10,18 +10,30 @@
#include "content/public/browser/service_worker_context_observer.h"
#include "gin/wrappable.h"
#include "shell/browser/event_emitter_mixin.h"
+#include "third_party/blink/public/common/service_worker/embedded_worker_status.h"
+
+namespace content {
+class StoragePartition;
+}
namespace gin {
template
class Handle;
} // namespace gin
+namespace gin_helper {
+template
+class Promise;
+} // namespace gin_helper
+
namespace electron {
class ElectronBrowserContext;
namespace api {
+class ServiceWorkerMain;
+
class ServiceWorkerContext final
: public gin::Wrappable,
public gin_helper::EventEmitterMixin,
@@ -32,14 +44,39 @@ class ServiceWorkerContext final
ElectronBrowserContext* browser_context);
v8::Local GetAllRunningWorkerInfo(v8::Isolate* isolate);
- v8::Local GetWorkerInfoFromID(gin_helper::ErrorThrower thrower,
- int64_t version_id);
+ v8::Local GetInfoFromVersionID(gin_helper::ErrorThrower thrower,
+ int64_t version_id);
+ v8::Local GetFromVersionID(gin_helper::ErrorThrower thrower,
+ int64_t version_id);
+ v8::Local GetWorkerFromVersionID(v8::Isolate* isolate,
+ int64_t version_id);
+ gin::Handle GetWorkerFromVersionIDIfExists(
+ v8::Isolate* isolate,
+ int64_t version_id);
+ v8::Local StartWorkerForScope(v8::Isolate* isolate, GURL scope);
+ void DidStartWorkerForScope(
+ std::shared_ptr>> shared_promise,
+ int64_t version_id,
+ int process_id,
+ int thread_id);
+ void DidFailToStartWorkerForScope(
+ std::shared_ptr>> shared_promise,
+ content::StatusCodeResponse status);
+ void StopWorkersForScope(GURL scope);
+ v8::Local StopAllWorkers(v8::Isolate* isolate);
// content::ServiceWorkerContextObserver
void OnReportConsoleMessage(int64_t version_id,
const GURL& scope,
const content::ConsoleMessage& message) override;
void OnRegistrationCompleted(const GURL& scope) override;
+ void OnVersionStartingRunning(int64_t version_id) override;
+ void OnVersionStartedRunning(
+ int64_t version_id,
+ const content::ServiceWorkerRunningInfo& running_info) override;
+ void OnVersionStoppingRunning(int64_t version_id) override;
+ void OnVersionStoppedRunning(int64_t version_id) override;
+ void OnVersionRedundant(int64_t version_id, const GURL& scope) override;
void OnDestruct(content::ServiceWorkerContext* context) override;
// gin::Wrappable
@@ -58,8 +95,15 @@ class ServiceWorkerContext final
~ServiceWorkerContext() override;
private:
+ void OnRunningStatusChanged(int64_t version_id,
+ blink::EmbeddedWorkerStatus running_status);
+
raw_ptr service_worker_context_;
+ // Service worker registration and versions are unique to a storage partition.
+ // Keep a reference to the storage partition to be used for lookups.
+ raw_ptr storage_partition_;
+
base::WeakPtrFactory weak_ptr_factory_{this};
};
diff --git a/shell/browser/api/electron_api_service_worker_main.cc b/shell/browser/api/electron_api_service_worker_main.cc
new file mode 100644
index 000000000000..8d24d872e054
--- /dev/null
+++ b/shell/browser/api/electron_api_service_worker_main.cc
@@ -0,0 +1,359 @@
+// Copyright (c) 2025 Salesforce, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/api/electron_api_service_worker_main.h"
+
+#include
+#include
+#include
+#include
+
+#include "base/logging.h"
+#include "base/no_destructor.h"
+#include "content/browser/service_worker/service_worker_context_wrapper.h" // nogncheck
+#include "content/browser/service_worker/service_worker_version.h" // nogncheck
+#include "electron/shell/common/api/api.mojom.h"
+#include "gin/handle.h"
+#include "gin/object_template_builder.h"
+#include "services/service_manager/public/cpp/interface_provider.h"
+#include "shell/browser/api/message_port.h"
+#include "shell/browser/browser.h"
+#include "shell/browser/javascript_environment.h"
+#include "shell/common/gin_converters/blink_converter.h"
+#include "shell/common/gin_converters/gurl_converter.h"
+#include "shell/common/gin_converters/value_converter.h"
+#include "shell/common/gin_helper/dictionary.h"
+#include "shell/common/gin_helper/error_thrower.h"
+#include "shell/common/gin_helper/object_template_builder.h"
+#include "shell/common/gin_helper/promise.h"
+#include "shell/common/node_includes.h"
+#include "shell/common/v8_util.h"
+
+namespace {
+
+// Use private API to get the live version of the service worker. This will
+// exist while in starting, stopping, or stopped running status.
+content::ServiceWorkerVersion* GetLiveVersion(
+ content::ServiceWorkerContext* service_worker_context,
+ int64_t version_id) {
+ auto* wrapper = static_cast(
+ service_worker_context);
+ return wrapper->GetLiveVersion(version_id);
+}
+
+// Get a public ServiceWorkerVersionBaseInfo object directly from the service
+// worker.
+std::optional GetLiveVersionInfo(
+ content::ServiceWorkerContext* service_worker_context,
+ int64_t version_id) {
+ auto* version = GetLiveVersion(service_worker_context, version_id);
+ if (version) {
+ return version->GetInfo();
+ }
+ return std::nullopt;
+}
+
+} // namespace
+
+namespace electron::api {
+
+// ServiceWorkerKey -> ServiceWorkerMain*
+typedef std::unordered_map
+ VersionIdMap;
+
+VersionIdMap& GetVersionIdMap() {
+ static base::NoDestructor instance;
+ return *instance;
+}
+
+ServiceWorkerMain* FromServiceWorkerKey(const ServiceWorkerKey& key) {
+ VersionIdMap& version_map = GetVersionIdMap();
+ auto iter = version_map.find(key);
+ auto* service_worker = iter == version_map.end() ? nullptr : iter->second;
+ return service_worker;
+}
+
+// static
+ServiceWorkerMain* ServiceWorkerMain::FromVersionID(
+ int64_t version_id,
+ const content::StoragePartition* storage_partition) {
+ ServiceWorkerKey key(version_id, storage_partition);
+ return FromServiceWorkerKey(key);
+}
+
+gin::WrapperInfo ServiceWorkerMain::kWrapperInfo = {gin::kEmbedderNativeGin};
+
+ServiceWorkerMain::ServiceWorkerMain(content::ServiceWorkerContext* sw_context,
+ int64_t version_id,
+ const ServiceWorkerKey& key)
+ : version_id_(version_id), key_(key), service_worker_context_(sw_context) {
+ GetVersionIdMap().emplace(key_, this);
+ InvalidateVersionInfo();
+}
+
+ServiceWorkerMain::~ServiceWorkerMain() {
+ Destroy();
+}
+
+void ServiceWorkerMain::Destroy() {
+ version_destroyed_ = true;
+ InvalidateVersionInfo();
+ MaybeDisconnectRemote();
+ GetVersionIdMap().erase(key_);
+ Unpin();
+}
+
+void ServiceWorkerMain::MaybeDisconnectRemote() {
+ if (remote_.is_bound() &&
+ (version_destroyed_ ||
+ (!service_worker_context_->IsLiveStartingServiceWorker(version_id_) &&
+ !service_worker_context_->IsLiveRunningServiceWorker(version_id_)))) {
+ remote_.reset();
+ }
+}
+
+mojom::ElectronRenderer* ServiceWorkerMain::GetRendererApi() {
+ if (!remote_.is_bound()) {
+ if (!service_worker_context_->IsLiveRunningServiceWorker(version_id_)) {
+ return nullptr;
+ }
+
+ service_worker_context_->GetRemoteAssociatedInterfaces(version_id_)
+ .GetInterface(&remote_);
+ }
+ return remote_.get();
+}
+
+void ServiceWorkerMain::Send(v8::Isolate* isolate,
+ bool internal,
+ const std::string& channel,
+ v8::Local args) {
+ blink::CloneableMessage message;
+ if (!gin::ConvertFromV8(isolate, args, &message)) {
+ isolate->ThrowException(v8::Exception::Error(
+ gin::StringToV8(isolate, "Failed to serialize arguments")));
+ return;
+ }
+
+ auto* renderer_api_remote = GetRendererApi();
+ if (!renderer_api_remote) {
+ return;
+ }
+
+ renderer_api_remote->Message(internal, channel, std::move(message));
+}
+
+void ServiceWorkerMain::InvalidateVersionInfo() {
+ if (version_info_ != nullptr) {
+ version_info_.reset();
+ }
+
+ if (version_destroyed_)
+ return;
+
+ auto version_info = GetLiveVersionInfo(service_worker_context_, version_id_);
+ if (version_info) {
+ version_info_ =
+ std::make_unique(*version_info);
+ } else {
+ // When ServiceWorkerContextCore::RemoveLiveVersion is called, it posts a
+ // task to notify that the service worker has stopped. At this point, the
+ // live version will no longer exist.
+ Destroy();
+ }
+}
+
+void ServiceWorkerMain::OnRunningStatusChanged(
+ blink::EmbeddedWorkerStatus running_status) {
+ // Disconnect remote when content::ServiceWorkerHost has terminated.
+ MaybeDisconnectRemote();
+
+ InvalidateVersionInfo();
+
+ // Redundant worker has been marked for deletion. Now that it's stopped, let's
+ // destroy our wrapper.
+ if (redundant_ && running_status == blink::EmbeddedWorkerStatus::kStopped) {
+ Destroy();
+ }
+}
+
+void ServiceWorkerMain::OnVersionRedundant() {
+ // Redundant service workers have been either unregistered or replaced. A new
+ // ServiceWorkerMain will need to be created.
+ // Set internal state to mark it for deletion once it has fully stopped.
+ redundant_ = true;
+}
+
+bool ServiceWorkerMain::IsDestroyed() const {
+ return version_destroyed_;
+}
+
+const blink::StorageKey ServiceWorkerMain::GetStorageKey() {
+ GURL scope = version_info_ ? version_info()->scope : GURL::EmptyGURL();
+ return blink::StorageKey::CreateFirstParty(url::Origin::Create(scope));
+}
+
+gin_helper::Dictionary ServiceWorkerMain::StartExternalRequest(
+ v8::Isolate* isolate,
+ bool has_timeout) {
+ auto details = gin_helper::Dictionary::CreateEmpty(isolate);
+
+ if (version_destroyed_) {
+ isolate->ThrowException(v8::Exception::TypeError(
+ gin::StringToV8(isolate, "ServiceWorkerMain is destroyed")));
+ return details;
+ }
+
+ auto request_uuid = base::Uuid::GenerateRandomV4();
+ auto timeout_type =
+ has_timeout
+ ? content::ServiceWorkerExternalRequestTimeoutType::kDefault
+ : content::ServiceWorkerExternalRequestTimeoutType::kDoesNotTimeout;
+
+ content::ServiceWorkerExternalRequestResult start_result =
+ service_worker_context_->StartingExternalRequest(
+ version_id_, timeout_type, request_uuid);
+
+ details.Set("id", request_uuid.AsLowercaseString());
+ details.Set("ok",
+ start_result == content::ServiceWorkerExternalRequestResult::kOk);
+
+ return details;
+}
+
+void ServiceWorkerMain::FinishExternalRequest(v8::Isolate* isolate,
+ std::string uuid) {
+ base::Uuid request_uuid = base::Uuid::ParseLowercase(uuid);
+ if (!request_uuid.is_valid()) {
+ isolate->ThrowException(v8::Exception::TypeError(
+ gin::StringToV8(isolate, "Invalid external request UUID")));
+ return;
+ }
+
+ DCHECK(service_worker_context_);
+ if (!service_worker_context_)
+ return;
+
+ content::ServiceWorkerExternalRequestResult result =
+ service_worker_context_->FinishedExternalRequest(version_id_,
+ request_uuid);
+
+ std::string error;
+ switch (result) {
+ case content::ServiceWorkerExternalRequestResult::kOk:
+ break;
+ case content::ServiceWorkerExternalRequestResult::kBadRequestId:
+ error = "Unknown external request UUID";
+ break;
+ case content::ServiceWorkerExternalRequestResult::kWorkerNotRunning:
+ error = "Service worker is no longer running";
+ break;
+ case content::ServiceWorkerExternalRequestResult::kWorkerNotFound:
+ error = "Service worker was not found";
+ break;
+ case content::ServiceWorkerExternalRequestResult::kNullContext:
+ default:
+ error = "Service worker context is unavailable and may be shutting down";
+ break;
+ }
+
+ if (!error.empty()) {
+ isolate->ThrowException(
+ v8::Exception::TypeError(gin::StringToV8(isolate, error)));
+ }
+}
+
+size_t ServiceWorkerMain::CountExternalRequestsForTest() {
+ if (version_destroyed_)
+ return 0;
+ auto& storage_key = GetStorageKey();
+ return service_worker_context_->CountExternalRequestsForTest(storage_key);
+}
+
+int64_t ServiceWorkerMain::VersionID() const {
+ return version_id_;
+}
+
+GURL ServiceWorkerMain::ScopeURL() const {
+ if (version_destroyed_)
+ return GURL::EmptyGURL();
+ return version_info()->scope;
+}
+
+// static
+gin::Handle ServiceWorkerMain::New(v8::Isolate* isolate) {
+ return gin::Handle();
+}
+
+// static
+gin::Handle ServiceWorkerMain::From(
+ v8::Isolate* isolate,
+ content::ServiceWorkerContext* sw_context,
+ const content::StoragePartition* storage_partition,
+ int64_t version_id) {
+ ServiceWorkerKey service_worker_key(version_id, storage_partition);
+
+ auto* service_worker = FromServiceWorkerKey(service_worker_key);
+ if (service_worker)
+ return gin::CreateHandle(isolate, service_worker);
+
+ // Ensure ServiceWorkerVersion exists and is not redundant (pending deletion)
+ auto* live_version = GetLiveVersion(sw_context, version_id);
+ if (!live_version || live_version->is_redundant()) {
+ return gin::Handle();
+ }
+
+ auto handle = gin::CreateHandle(
+ isolate,
+ new ServiceWorkerMain(sw_context, version_id, service_worker_key));
+
+ // Prevent garbage collection of worker until it has been deleted internally.
+ handle->Pin(isolate);
+
+ return handle;
+}
+
+// static
+void ServiceWorkerMain::FillObjectTemplate(
+ v8::Isolate* isolate,
+ v8::Local templ) {
+ gin_helper::ObjectTemplateBuilder(isolate, templ)
+ .SetMethod("_send", &ServiceWorkerMain::Send)
+ .SetMethod("isDestroyed", &ServiceWorkerMain::IsDestroyed)
+ .SetMethod("_startExternalRequest",
+ &ServiceWorkerMain::StartExternalRequest)
+ .SetMethod("_finishExternalRequest",
+ &ServiceWorkerMain::FinishExternalRequest)
+ .SetMethod("_countExternalRequests",
+ &ServiceWorkerMain::CountExternalRequestsForTest)
+ .SetProperty("versionId", &ServiceWorkerMain::VersionID)
+ .SetProperty("scope", &ServiceWorkerMain::ScopeURL)
+ .Build();
+}
+
+const char* ServiceWorkerMain::GetTypeName() {
+ return GetClassName();
+}
+
+} // namespace electron::api
+
+namespace {
+
+using electron::api::ServiceWorkerMain;
+
+void Initialize(v8::Local exports,
+ v8::Local unused,
+ v8::Local context,
+ void* priv) {
+ v8::Isolate* isolate = context->GetIsolate();
+ gin_helper::Dictionary dict(isolate, exports);
+ dict.Set("ServiceWorkerMain", ServiceWorkerMain::GetConstructor(context));
+}
+
+} // namespace
+
+NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_service_worker_main,
+ Initialize)
diff --git a/shell/browser/api/electron_api_service_worker_main.h b/shell/browser/api/electron_api_service_worker_main.h
new file mode 100644
index 000000000000..995f7ad5428b
--- /dev/null
+++ b/shell/browser/api/electron_api_service_worker_main.h
@@ -0,0 +1,178 @@
+// Copyright (c) 2025 Salesforce, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_BROWSER_API_ELECTRON_API_SERVICE_WORKER_MAIN_H_
+#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_SERVICE_WORKER_MAIN_H_
+
+#include
+#include
+#include
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/process/process.h"
+#include "content/public/browser/global_routing_id.h"
+#include "content/public/browser/service_worker_context.h"
+#include "content/public/browser/service_worker_version_base_info.h"
+#include "gin/wrappable.h"
+#include "mojo/public/cpp/bindings/associated_receiver.h"
+#include "mojo/public/cpp/bindings/associated_remote.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "shell/browser/event_emitter_mixin.h"
+#include "shell/common/api/api.mojom.h"
+#include "shell/common/gin_helper/constructible.h"
+#include "shell/common/gin_helper/pinnable.h"
+#include "third_party/blink/public/common/service_worker/embedded_worker_status.h"
+
+class GURL;
+
+namespace content {
+class StoragePartition;
+}
+
+namespace gin {
+class Arguments;
+} // namespace gin
+
+namespace gin_helper {
+class Dictionary;
+template
+class Handle;
+template
+class Promise;
+} // namespace gin_helper
+
+namespace electron::api {
+
+// Key to uniquely identify a ServiceWorkerMain by its Version ID within the
+// associated StoragePartition.
+struct ServiceWorkerKey {
+ int64_t version_id;
+ raw_ptr storage_partition;
+
+ ServiceWorkerKey(int64_t id, const content::StoragePartition* partition)
+ : version_id(id), storage_partition(partition) {}
+
+ bool operator<(const ServiceWorkerKey& other) const {
+ return std::tie(version_id, storage_partition) <
+ std::tie(other.version_id, other.storage_partition);
+ }
+
+ bool operator==(const ServiceWorkerKey& other) const {
+ return version_id == other.version_id &&
+ storage_partition == other.storage_partition;
+ }
+
+ struct Hasher {
+ std::size_t operator()(const ServiceWorkerKey& key) const {
+ return std::hash()(
+ key.storage_partition) ^
+ std::hash()(key.version_id);
+ }
+ };
+};
+
+// Creates a wrapper to align with the lifecycle of the non-public
+// content::ServiceWorkerVersion. Object instances are pinned for the lifetime
+// of the underlying SW such that registered IPC handlers continue to dispatch.
+//
+// Instances are uniquely identified by pairing their version ID and the
+// StoragePartition in which they're registered. In Electron, this is always
+// the default StoragePartition for the associated BrowserContext.
+class ServiceWorkerMain final
+ : public gin::Wrappable,
+ public gin_helper::EventEmitterMixin,
+ public gin_helper::Pinnable,
+ public gin_helper::Constructible {
+ public:
+ // Create a new ServiceWorkerMain and return the V8 wrapper of it.
+ static gin::Handle New(v8::Isolate* isolate);
+
+ static gin::Handle From(
+ v8::Isolate* isolate,
+ content::ServiceWorkerContext* sw_context,
+ const content::StoragePartition* storage_partition,
+ int64_t version_id);
+ static ServiceWorkerMain* FromVersionID(
+ int64_t version_id,
+ const content::StoragePartition* storage_partition);
+
+ // gin_helper::Constructible
+ static void FillObjectTemplate(v8::Isolate*, v8::Local);
+ static const char* GetClassName() { return "ServiceWorkerMain"; }
+
+ // gin::Wrappable
+ static gin::WrapperInfo kWrapperInfo;
+ const char* GetTypeName() override;
+
+ // disable copy
+ ServiceWorkerMain(const ServiceWorkerMain&) = delete;
+ ServiceWorkerMain& operator=(const ServiceWorkerMain&) = delete;
+
+ void OnRunningStatusChanged(blink::EmbeddedWorkerStatus running_status);
+ void OnVersionRedundant();
+
+ protected:
+ explicit ServiceWorkerMain(content::ServiceWorkerContext* sw_context,
+ int64_t version_id,
+ const ServiceWorkerKey& key);
+ ~ServiceWorkerMain() override;
+
+ private:
+ void Destroy();
+ void MaybeDisconnectRemote();
+ const blink::StorageKey GetStorageKey();
+
+ // Increments external requests for the service worker to keep it alive.
+ gin_helper::Dictionary StartExternalRequest(v8::Isolate* isolate,
+ bool has_timeout);
+ void FinishExternalRequest(v8::Isolate* isolate, std::string uuid);
+ size_t CountExternalRequestsForTest();
+
+ // Get or create a Mojo connection to the renderer process.
+ mojom::ElectronRenderer* GetRendererApi();
+
+ // Send a message to the renderer process.
+ void Send(v8::Isolate* isolate,
+ bool internal,
+ const std::string& channel,
+ v8::Local args);
+
+ void InvalidateVersionInfo();
+ const content::ServiceWorkerVersionBaseInfo* version_info() const {
+ return version_info_.get();
+ }
+
+ bool IsDestroyed() const;
+
+ int64_t VersionID() const;
+ GURL ScopeURL() const;
+
+ // Version ID unique only to the StoragePartition.
+ int64_t version_id_;
+
+ // Unique identifier pairing the Version ID and StoragePartition.
+ ServiceWorkerKey key_;
+
+ // Whether the Service Worker version has been destroyed.
+ bool version_destroyed_ = false;
+
+ // Whether the Service Worker version's state is redundant.
+ bool redundant_ = false;
+
+ // Store copy of version info so it's accessible when not running.
+ std::unique_ptr version_info_;
+
+ raw_ptr service_worker_context_;
+ mojo::AssociatedRemote remote_;
+
+ std::unique_ptr> start_worker_promise_;
+
+ base::WeakPtrFactory weak_factory_{this};
+};
+
+} // namespace electron::api
+
+#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_SERVICE_WORKER_MAIN_H_
diff --git a/shell/common/gin_converters/service_worker_converter.cc b/shell/common/gin_converters/service_worker_converter.cc
new file mode 100644
index 000000000000..56eaee64d53e
--- /dev/null
+++ b/shell/common/gin_converters/service_worker_converter.cc
@@ -0,0 +1,25 @@
+// Copyright (c) 2025 Salesforce, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/common/gin_converters/service_worker_converter.h"
+
+#include "base/containers/fixed_flat_map.h"
+
+namespace gin {
+
+// static
+v8::Local Converter::ToV8(
+ v8::Isolate* isolate,
+ const blink::EmbeddedWorkerStatus& val) {
+ static constexpr auto Lookup =
+ base::MakeFixedFlatMap({
+ {blink::EmbeddedWorkerStatus::kStarting, "starting"},
+ {blink::EmbeddedWorkerStatus::kRunning, "running"},
+ {blink::EmbeddedWorkerStatus::kStopping, "stopping"},
+ {blink::EmbeddedWorkerStatus::kStopped, "stopped"},
+ });
+ return StringToV8(isolate, Lookup.at(val));
+}
+
+} // namespace gin
diff --git a/shell/common/gin_converters/service_worker_converter.h b/shell/common/gin_converters/service_worker_converter.h
new file mode 100644
index 000000000000..33f4ec23ef01
--- /dev/null
+++ b/shell/common/gin_converters/service_worker_converter.h
@@ -0,0 +1,21 @@
+// Copyright (c) 2025 Salesforce, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_COMMON_GIN_CONVERTERS_SERVICE_WORKER_CONVERTER_H_
+#define ELECTRON_SHELL_COMMON_GIN_CONVERTERS_SERVICE_WORKER_CONVERTER_H_
+
+#include "gin/converter.h"
+#include "third_party/blink/public/common/service_worker/embedded_worker_status.h"
+
+namespace gin {
+
+template <>
+struct Converter {
+ static v8::Local ToV8(v8::Isolate* isolate,
+ const blink::EmbeddedWorkerStatus& val);
+};
+
+} // namespace gin
+
+#endif // ELECTRON_SHELL_COMMON_GIN_CONVERTERS_SERVICE_WORKER_CONVERTER_H_
diff --git a/shell/common/node_bindings.cc b/shell/common/node_bindings.cc
index 484947cee4be..bde4c3ca5d1d 100644
--- a/shell/common/node_bindings.cc
+++ b/shell/common/node_bindings.cc
@@ -49,39 +49,40 @@
#include "shell/common/crash_keys.h"
#endif
-#define ELECTRON_BROWSER_BINDINGS(V) \
- V(electron_browser_app) \
- V(electron_browser_auto_updater) \
- V(electron_browser_content_tracing) \
- V(electron_browser_crash_reporter) \
- V(electron_browser_desktop_capturer) \
- V(electron_browser_dialog) \
- V(electron_browser_event_emitter) \
- V(electron_browser_global_shortcut) \
- V(electron_browser_image_view) \
- V(electron_browser_in_app_purchase) \
- V(electron_browser_menu) \
- V(electron_browser_message_port) \
- V(electron_browser_native_theme) \
- V(electron_browser_notification) \
- V(electron_browser_power_monitor) \
- V(electron_browser_power_save_blocker) \
- V(electron_browser_protocol) \
- V(electron_browser_printing) \
- V(electron_browser_push_notifications) \
- V(electron_browser_safe_storage) \
- V(electron_browser_session) \
- V(electron_browser_screen) \
- V(electron_browser_system_preferences) \
- V(electron_browser_base_window) \
- V(electron_browser_tray) \
- V(electron_browser_utility_process) \
- V(electron_browser_view) \
- V(electron_browser_web_contents) \
- V(electron_browser_web_contents_view) \
- V(electron_browser_web_frame_main) \
- V(electron_browser_web_view_manager) \
- V(electron_browser_window) \
+#define ELECTRON_BROWSER_BINDINGS(V) \
+ V(electron_browser_app) \
+ V(electron_browser_auto_updater) \
+ V(electron_browser_content_tracing) \
+ V(electron_browser_crash_reporter) \
+ V(electron_browser_desktop_capturer) \
+ V(electron_browser_dialog) \
+ V(electron_browser_event_emitter) \
+ V(electron_browser_global_shortcut) \
+ V(electron_browser_image_view) \
+ V(electron_browser_in_app_purchase) \
+ V(electron_browser_menu) \
+ V(electron_browser_message_port) \
+ V(electron_browser_native_theme) \
+ V(electron_browser_notification) \
+ V(electron_browser_power_monitor) \
+ V(electron_browser_power_save_blocker) \
+ V(electron_browser_protocol) \
+ V(electron_browser_printing) \
+ V(electron_browser_push_notifications) \
+ V(electron_browser_safe_storage) \
+ V(electron_browser_service_worker_main) \
+ V(electron_browser_session) \
+ V(electron_browser_screen) \
+ V(electron_browser_system_preferences) \
+ V(electron_browser_base_window) \
+ V(electron_browser_tray) \
+ V(electron_browser_utility_process) \
+ V(electron_browser_view) \
+ V(electron_browser_web_contents) \
+ V(electron_browser_web_contents_view) \
+ V(electron_browser_web_frame_main) \
+ V(electron_browser_web_view_manager) \
+ V(electron_browser_window) \
V(electron_common_net)
#define ELECTRON_COMMON_BINDINGS(V) \
diff --git a/spec/api-service-worker-main-spec.ts b/spec/api-service-worker-main-spec.ts
new file mode 100644
index 000000000000..0bc404c314d8
--- /dev/null
+++ b/spec/api-service-worker-main-spec.ts
@@ -0,0 +1,291 @@
+import { session, webContents as webContentsModule, WebContents } from 'electron/main';
+
+import { expect } from 'chai';
+
+import { once, on } from 'node:events';
+import * as fs from 'node:fs';
+import * as http from 'node:http';
+import * as path from 'node:path';
+
+import { listen, waitUntil } from './lib/spec-helpers';
+
+// Toggle to add extra debug output
+const DEBUG = !process.env.CI;
+
+describe('ServiceWorkerMain module', () => {
+ const fixtures = path.resolve(__dirname, 'fixtures');
+ const webContentsInternal: typeof ElectronInternal.WebContents = webContentsModule as any;
+
+ let ses: Electron.Session;
+ let serviceWorkers: Electron.ServiceWorkers;
+ let server: http.Server;
+ let baseUrl: string;
+ let wc: WebContents;
+
+ beforeEach(async () => {
+ ses = session.fromPartition(`service-worker-main-spec-${crypto.randomUUID()}`);
+ serviceWorkers = ses.serviceWorkers;
+
+ if (DEBUG) {
+ serviceWorkers.on('console-message', (_e, details) => {
+ console.log(details.message);
+ });
+ serviceWorkers.on('running-status-changed', ({ versionId, runningStatus }) => {
+ console.log(`version ${versionId} is ${runningStatus}`);
+ });
+ }
+
+ const uuid = crypto.randomUUID();
+ server = http.createServer((req, res) => {
+ const url = new URL(req.url!, `http://${req.headers.host}`);
+ // /{uuid}/{file}
+ const file = url.pathname!.split('/')[2]!;
+
+ if (file.endsWith('.js')) {
+ res.setHeader('Content-Type', 'application/javascript');
+ }
+ res.end(fs.readFileSync(path.resolve(fixtures, 'api', 'service-workers', file)));
+ });
+ const { port } = await listen(server);
+ baseUrl = `http://localhost:${port}/${uuid}`;
+
+ wc = webContentsInternal.create({ session: ses });
+
+ if (DEBUG) {
+ wc.on('console-message', ({ message }) => {
+ console.log(message);
+ });
+ }
+ });
+
+ afterEach(async () => {
+ if (!wc.isDestroyed()) wc.destroy();
+ server.close();
+ });
+
+ async function loadWorkerScript (scriptUrl?: string) {
+ const scriptParams = scriptUrl ? `?scriptUrl=${scriptUrl}` : '';
+ return wc.loadURL(`${baseUrl}/index.html${scriptParams}`);
+ }
+
+ async function unregisterAllServiceWorkers () {
+ await wc.executeJavaScript(`(${async function () {
+ const registrations = await navigator.serviceWorker.getRegistrations();
+ for (const registration of registrations) {
+ registration.unregister();
+ }
+ }}())`);
+ }
+
+ async function waitForServiceWorker (expectedRunningStatus: Electron.ServiceWorkersRunningStatusChangedEventParams['runningStatus'] = 'starting') {
+ const serviceWorkerPromise = new Promise((resolve) => {
+ function onRunningStatusChanged ({ versionId, runningStatus }: Electron.ServiceWorkersRunningStatusChangedEventParams) {
+ if (runningStatus === expectedRunningStatus) {
+ const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId)!;
+ serviceWorkers.off('running-status-changed', onRunningStatusChanged);
+ resolve(serviceWorker);
+ }
+ }
+ serviceWorkers.on('running-status-changed', onRunningStatusChanged);
+ });
+ const serviceWorker = await serviceWorkerPromise;
+ expect(serviceWorker).to.not.be.undefined();
+ return serviceWorker!;
+ }
+
+ describe('serviceWorkers.getWorkerFromVersionID', () => {
+ it('returns undefined for non-live service worker', () => {
+ expect(serviceWorkers.getWorkerFromVersionID(-1)).to.be.undefined();
+ expect(serviceWorkers._getWorkerFromVersionIDIfExists(-1)).to.be.undefined();
+ });
+
+ it('returns instance for live service worker', async () => {
+ const runningStatusChanged = once(serviceWorkers, 'running-status-changed');
+ loadWorkerScript();
+ const [{ versionId }] = await runningStatusChanged;
+ const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId);
+ expect(serviceWorker).to.not.be.undefined();
+ const ifExistsServiceWorker = serviceWorkers._getWorkerFromVersionIDIfExists(versionId);
+ expect(ifExistsServiceWorker).to.not.be.undefined();
+ expect(serviceWorker).to.equal(ifExistsServiceWorker);
+ });
+
+ it('does not crash on script error', async () => {
+ wc.loadURL(`${baseUrl}/index.html?scriptUrl=sw-script-error.js`);
+ let serviceWorker;
+ const actualStatuses = [];
+ for await (const [{ versionId, runningStatus }] of on(serviceWorkers, 'running-status-changed')) {
+ if (!serviceWorker) {
+ serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId);
+ }
+ actualStatuses.push(runningStatus);
+ if (runningStatus === 'stopping') {
+ break;
+ }
+ }
+ expect(actualStatuses).to.deep.equal(['starting', 'stopping']);
+ expect(serviceWorker).to.not.be.undefined();
+ });
+
+ it('does not find unregistered service worker', async () => {
+ loadWorkerScript();
+ const runningServiceWorker = await waitForServiceWorker('running');
+ const { versionId } = runningServiceWorker;
+ unregisterAllServiceWorkers();
+ await waitUntil(() => runningServiceWorker.isDestroyed());
+ const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId);
+ expect(serviceWorker).to.be.undefined();
+ });
+ });
+
+ describe('isDestroyed()', () => {
+ it('is not destroyed after being created', async () => {
+ loadWorkerScript();
+ const serviceWorker = await waitForServiceWorker();
+ expect(serviceWorker.isDestroyed()).to.be.false();
+ });
+
+ it('is destroyed after being unregistered', async () => {
+ loadWorkerScript();
+ const serviceWorker = await waitForServiceWorker();
+ expect(serviceWorker.isDestroyed()).to.be.false();
+ await unregisterAllServiceWorkers();
+ await waitUntil(() => serviceWorker.isDestroyed());
+ });
+ });
+
+ describe('"running-status-changed" event', () => {
+ it('handles when content::ServiceWorkerVersion has been destroyed', async () => {
+ loadWorkerScript('sw-unregister-self.js');
+ const serviceWorker = await waitForServiceWorker('running');
+ await waitUntil(() => serviceWorker.isDestroyed());
+ });
+ });
+
+ describe('startWorkerForScope()', () => {
+ it('resolves with running workers', async () => {
+ loadWorkerScript();
+ const serviceWorker = await waitForServiceWorker('running');
+ const startWorkerPromise = serviceWorkers.startWorkerForScope(serviceWorker.scope);
+ await expect(startWorkerPromise).to.eventually.be.fulfilled();
+ const otherSW = await startWorkerPromise;
+ expect(otherSW).to.equal(serviceWorker);
+ });
+
+ it('rejects with starting workers', async () => {
+ loadWorkerScript();
+ const serviceWorker = await waitForServiceWorker('starting');
+ const startWorkerPromise = serviceWorkers.startWorkerForScope(serviceWorker.scope);
+ await expect(startWorkerPromise).to.eventually.be.rejected();
+ });
+
+ it('starts previously stopped worker', async () => {
+ loadWorkerScript();
+ const serviceWorker = await waitForServiceWorker('running');
+ const { scope } = serviceWorker;
+ const stoppedPromise = waitForServiceWorker('stopped');
+ await serviceWorkers._stopAllWorkers();
+ await stoppedPromise;
+ const startWorkerPromise = serviceWorkers.startWorkerForScope(scope);
+ await expect(startWorkerPromise).to.eventually.be.fulfilled();
+ });
+
+ it('resolves when called twice', async () => {
+ loadWorkerScript();
+ const serviceWorker = await waitForServiceWorker('running');
+ const { scope } = serviceWorker;
+ const [swA, swB] = await Promise.all([
+ serviceWorkers.startWorkerForScope(scope),
+ serviceWorkers.startWorkerForScope(scope)
+ ]);
+ expect(swA).to.equal(swB);
+ expect(swA).to.equal(serviceWorker);
+ });
+ });
+
+ describe('startTask()', () => {
+ it('has no tasks in-flight initially', async () => {
+ loadWorkerScript();
+ const serviceWorker = await waitForServiceWorker();
+ expect(serviceWorker._countExternalRequests()).to.equal(0);
+ });
+
+ it('can start and end a task', async () => {
+ loadWorkerScript();
+
+ // Internally, ServiceWorkerVersion buckets tasks into requests made
+ // during and after startup.
+ // ServiceWorkerContext::CountExternalRequestsForTest only considers
+ // requests made while SW is in running status so we need to wait for that
+ // to read an accurate count.
+ const serviceWorker = await waitForServiceWorker('running');
+
+ const task = serviceWorker.startTask();
+ expect(task).to.be.an('object');
+ expect(task).to.have.property('end').that.is.a('function');
+ expect(serviceWorker._countExternalRequests()).to.equal(1);
+
+ task.end();
+
+ // Count will decrement after Promise.finally callback
+ await new Promise(queueMicrotask);
+ expect(serviceWorker._countExternalRequests()).to.equal(0);
+ });
+
+ it('can have more than one active task', async () => {
+ loadWorkerScript();
+ const serviceWorker = await waitForServiceWorker('running');
+
+ const taskA = serviceWorker.startTask();
+ const taskB = serviceWorker.startTask();
+ expect(serviceWorker._countExternalRequests()).to.equal(2);
+ taskB.end();
+ taskA.end();
+
+ // Count will decrement after Promise.finally callback
+ await new Promise(queueMicrotask);
+ expect(serviceWorker._countExternalRequests()).to.equal(0);
+ });
+
+ it('throws when starting task after destroyed', async () => {
+ loadWorkerScript();
+ const serviceWorker = await waitForServiceWorker();
+ await unregisterAllServiceWorkers();
+ await waitUntil(() => serviceWorker.isDestroyed());
+ expect(() => serviceWorker.startTask()).to.throw();
+ });
+
+ it('throws when ending task after destroyed', async function () {
+ loadWorkerScript();
+ const serviceWorker = await waitForServiceWorker();
+ const task = serviceWorker.startTask();
+ await unregisterAllServiceWorkers();
+ await waitUntil(() => serviceWorker.isDestroyed());
+ expect(() => task.end()).to.throw();
+ });
+ });
+
+ describe("'versionId' property", () => {
+ it('matches the expected value', async () => {
+ const runningStatusChanged = once(serviceWorkers, 'running-status-changed');
+ wc.loadURL(`${baseUrl}/index.html`);
+ const [{ versionId }] = await runningStatusChanged;
+ const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId);
+ expect(serviceWorker).to.not.be.undefined();
+ if (!serviceWorker) return;
+ expect(serviceWorker).to.have.property('versionId').that.is.a('number');
+ expect(serviceWorker.versionId).to.equal(versionId);
+ });
+ });
+
+ describe("'scope' property", () => {
+ it('matches the expected value', async () => {
+ loadWorkerScript();
+ const serviceWorker = await waitForServiceWorker();
+ expect(serviceWorker).to.not.be.undefined();
+ if (!serviceWorker) return;
+ expect(serviceWorker).to.have.property('scope').that.is.a('string');
+ expect(serviceWorker.scope).to.equal(`${baseUrl}/`);
+ });
+ });
+});
diff --git a/spec/fixtures/api/service-workers/index.html b/spec/fixtures/api/service-workers/index.html
index 81c54d201b08..58c729d5c735 100644
--- a/spec/fixtures/api/service-workers/index.html
+++ b/spec/fixtures/api/service-workers/index.html
@@ -2,7 +2,8 @@
diff --git a/spec/fixtures/api/service-workers/sw-script-error.js b/spec/fixtures/api/service-workers/sw-script-error.js
new file mode 100644
index 000000000000..46b09e75dc58
--- /dev/null
+++ b/spec/fixtures/api/service-workers/sw-script-error.js
@@ -0,0 +1 @@
+throw new Error('service worker throwing on startup');
diff --git a/spec/fixtures/api/service-workers/sw-unregister-self.js b/spec/fixtures/api/service-workers/sw-unregister-self.js
new file mode 100644
index 000000000000..b3287c154b8a
--- /dev/null
+++ b/spec/fixtures/api/service-workers/sw-unregister-self.js
@@ -0,0 +1,3 @@
+self.addEventListener('install', function () {
+ registration.unregister();
+});
diff --git a/typings/internal-ambient.d.ts b/typings/internal-ambient.d.ts
index 4b5470e775df..25196e752f8c 100644
--- a/typings/internal-ambient.d.ts
+++ b/typings/internal-ambient.d.ts
@@ -111,6 +111,10 @@ declare namespace NodeJS {
setListeningForShutdown(listening: boolean): void;
}
+ interface ServiceWorkerMainBinding {
+ ServiceWorkerMain: typeof Electron.ServiceWorkerMain;
+ }
+
interface SessionBinding {
fromPartition: typeof Electron.Session.fromPartition,
fromPath: typeof Electron.Session.fromPath,
@@ -228,6 +232,7 @@ declare namespace NodeJS {
_linkedBinding(name: 'electron_browser_safe_storage'): { safeStorage: Electron.SafeStorage };
_linkedBinding(name: 'electron_browser_session'): SessionBinding;
_linkedBinding(name: 'electron_browser_screen'): { createScreen(): Electron.Screen };
+ _linkedBinding(name: 'electron_browser_service_worker_main'): ServiceWorkerMainBinding;
_linkedBinding(name: 'electron_browser_system_preferences'): { systemPreferences: Electron.SystemPreferences };
_linkedBinding(name: 'electron_browser_tray'): { Tray: Electron.Tray };
_linkedBinding(name: 'electron_browser_view'): { View: Electron.View };
diff --git a/typings/internal-electron.d.ts b/typings/internal-electron.d.ts
index 1ab4507af12d..c7cac707b56f 100644
--- a/typings/internal-electron.d.ts
+++ b/typings/internal-electron.d.ts
@@ -66,6 +66,18 @@ declare namespace Electron {
}
}
+ interface ServiceWorkers {
+ _getWorkerFromVersionIDIfExists(versionId: number): Electron.ServiceWorkerMain | undefined;
+ _stopAllWorkers(): Promise;
+ }
+
+ interface ServiceWorkerMain {
+ _startExternalRequest(hasTimeout: boolean): { id: string, ok: boolean };
+ _finishExternalRequest(uuid: string): void;
+ _countExternalRequests(): number;
+ }
+
+
interface TouchBar {
_removeFromWindow: (win: BaseWindow) => void;
}