feat: ServiceWorkerMain

This commit is contained in:
Samuel Maddock
2024-10-28 18:51:32 -04:00
parent 6a730a8e80
commit f59d8d618a
19 changed files with 980 additions and 42 deletions

View File

@@ -128,6 +128,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)

View File

@@ -0,0 +1,75 @@
# 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)<br />
_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.send(channel, ...args)` _Experimental_
- `channel` string
- `...args` any[]
Send an asynchronous message to the service worker process via `channel`, along with
arguments. Arguments will be serialized with the [Structured Clone Algorithm][SCA],
just like [`postMessage`][], so prototype chains will not be included.
Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an exception.
The service worker process can handle the message by listening to `channel` with the
[`ipcRenderer`](ipc-renderer.md) module.
#### `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.
```js
const { session } = require('electron')
const { serviceWorkers } = session.defaultSession
async function fetchData () {}
const versionId = 0
const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId)
serviceWorker?.ipc.handle('request-data', async () => {
// Keep service worker alive while fetching data
const task = serviceWorker.startTask()
try {
return await fetchData()
} finally {
// Mark task as ended to allow service worker to terminate when idle.
task.end()
}
})
```
### Instance Properties
#### `serviceWorker.ipc` _Readonly_ _Experimental_
An [`IpcMainServiceWorker`](ipc-main-service-worker.md) instance scoped to the service worker.
#### `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.
[SCA]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[`postMessage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

View File

@@ -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,55 @@ The following methods are available on instances of `ServiceWorkers`:
Returns `Record<number, ServiceWorkerInfo>` - 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.
#### `serviceWorkers.startWorkerForScope(scope)` _Experimental_
* `scope` string - The scope of the service worker to start.
Returns `Promise<ServiceWorkerMain>` - 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 and send message
const serviceWorker = await serviceWorkers.startWorkerForScope(scope)
serviceWorker.send('window-created', { windowId: window.id })
} catch (error) {
console.error(`Failed to start service worker for ${scope}`)
console.error(error)
}
}
})
```

View File

@@ -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

View File

@@ -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)
```
## Planned Breaking API Changes (34.0)
### Deprecated: `level`, `message`, `line`, and `sourceId` arguments in `console-message` event on `WebContents`

View File

@@ -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",
@@ -242,6 +243,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",

View File

@@ -304,6 +304,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",
@@ -622,6 +624,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",

View File

@@ -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') },

View File

@@ -0,0 +1,39 @@
import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';
const { ServiceWorkerMain } = process._linkedBinding('electron_browser_service_worker_main');
Object.defineProperty(ServiceWorkerMain.prototype, 'ipc', {
get () {
const ipc = new IpcMainImpl();
Object.defineProperty(this, 'ipc', { value: ipc });
return ipc;
}
});
ServiceWorkerMain.prototype.send = function (channel, ...args) {
if (typeof channel !== 'string') {
throw new TypeError('Missing required channel argument');
}
try {
return this._send(false /* internal */, channel, args);
} catch (e) {
console.error('Error sending from ServiceWorkerMain: ', e);
}
};
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;

View File

@@ -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');

View File

@@ -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();
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<v8::Value> ServiceWorkerContext::GetAllRunningWorkerInfo(
return builder.Build();
}
v8::Local<v8::Value> ServiceWorkerContext::GetWorkerInfoFromID(
v8::Local<v8::Value> ServiceWorkerContext::GetInfoFromVersionID(
gin_helper::ErrorThrower thrower,
int64_t version_id) {
const base::flat_map<int64_t, content::ServiceWorkerRunningInfo>& info_map =
@@ -138,6 +188,87 @@ v8::Local<v8::Value> ServiceWorkerContext::GetWorkerInfoFromID(
std::move(iter->second));
}
v8::Local<v8::Value> 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<v8::Value> ServiceWorkerContext::GetWorkerFromVersionID(
v8::Isolate* isolate,
int64_t version_id) {
return ServiceWorkerMain::From(isolate, service_worker_context_,
storage_partition_, version_id)
.ToV8();
}
gin::Handle<ServiceWorkerMain>
ServiceWorkerContext::GetWorkerFromVersionIDIfExists(v8::Isolate* isolate,
int64_t version_id) {
ServiceWorkerMain* worker =
ServiceWorkerMain::FromVersionID(version_id, storage_partition_);
if (!worker)
return gin::Handle<ServiceWorkerMain>();
return gin::CreateHandle(isolate, worker);
}
v8::Local<v8::Promise> ServiceWorkerContext::StartWorkerForScope(
v8::Isolate* isolate,
GURL scope) {
auto shared_promise =
std::make_shared<gin_helper::Promise<v8::Local<v8::Value>>>(isolate);
v8::Local<v8::Promise> 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<gin_helper::Promise<v8::Local<v8::Value>>> 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<v8::Value> service_worker_main =
GetWorkerFromVersionID(isolate, version_id);
shared_promise->Resolve(service_worker_main);
shared_promise.reset();
}
void ServiceWorkerContext::DidFailToStartWorkerForScope(
std::shared_ptr<gin_helper::Promise<v8::Local<v8::Value>>> shared_promise,
blink::ServiceWorkerStatusCode status_code) {
shared_promise->RejectWithErrorMessage("Failed to start service worker.");
shared_promise.reset();
}
v8::Local<v8::Promise> ServiceWorkerContext::StopAllWorkers(
v8::Isolate* isolate) {
auto promise = gin_helper::Promise<void>(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
service_worker_context_->StopAllServiceWorkers(base::BindOnce(
[](gin_helper::Promise<void> promise) { promise.Resolve(); },
std::move(promise)));
return handle;
}
// static
gin::Handle<ServiceWorkerContext> 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() {

View File

@@ -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 <typename T>
class Handle;
} // namespace gin
namespace gin_helper {
template <typename T>
class Promise;
} // namespace gin_helper
namespace electron {
class ElectronBrowserContext;
namespace api {
class ServiceWorkerMain;
class ServiceWorkerContext final
: public gin::Wrappable<ServiceWorkerContext>,
public gin_helper::EventEmitterMixin<ServiceWorkerContext>,
@@ -32,14 +44,39 @@ class ServiceWorkerContext final
ElectronBrowserContext* browser_context);
v8::Local<v8::Value> GetAllRunningWorkerInfo(v8::Isolate* isolate);
v8::Local<v8::Value> GetWorkerInfoFromID(gin_helper::ErrorThrower thrower,
int64_t version_id);
v8::Local<v8::Value> GetInfoFromVersionID(gin_helper::ErrorThrower thrower,
int64_t version_id);
v8::Local<v8::Value> GetFromVersionID(gin_helper::ErrorThrower thrower,
int64_t version_id);
v8::Local<v8::Value> GetWorkerFromVersionID(v8::Isolate* isolate,
int64_t version_id);
gin::Handle<ServiceWorkerMain> GetWorkerFromVersionIDIfExists(
v8::Isolate* isolate,
int64_t version_id);
v8::Local<v8::Promise> StartWorkerForScope(v8::Isolate* isolate, GURL scope);
void DidStartWorkerForScope(
std::shared_ptr<gin_helper::Promise<v8::Local<v8::Value>>> shared_promise,
int64_t version_id,
int process_id,
int thread_id);
void DidFailToStartWorkerForScope(
std::shared_ptr<gin_helper::Promise<v8::Local<v8::Value>>> shared_promise,
blink::ServiceWorkerStatusCode status_code);
void StopWorkersForScope(GURL scope);
v8::Local<v8::Promise> 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<content::ServiceWorkerContext> 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<content::StoragePartition> storage_partition_;
base::WeakPtrFactory<ServiceWorkerContext> weak_ptr_factory_{this};
};

View File

@@ -0,0 +1,319 @@
// 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 <string>
#include <unordered_map>
#include <utility>
#include <vector>
#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<content::ServiceWorkerContextWrapper*>(
service_worker_context);
return wrapper->GetLiveVersion(version_id);
}
// Get a public ServiceWorkerVersionBaseInfo object directly from the service
// worker.
std::optional<content::ServiceWorkerVersionBaseInfo> 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<ServiceWorkerKey,
ServiceWorkerMain*,
ServiceWorkerKey::Hasher>
VersionIdMap;
VersionIdMap& GetVersionIdMap() {
static base::NoDestructor<VersionIdMap> 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();
GetVersionIdMap().erase(key_);
Unpin();
}
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<v8::Value> 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<content::ServiceWorkerVersionBaseInfo>(*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() {
InvalidateVersionInfo();
// Disconnect remote when content::ServiceWorkerHost has terminated.
if (remote_.is_bound() &&
!service_worker_context_->IsLiveStartingServiceWorker(version_id_) &&
!service_worker_context_->IsLiveRunningServiceWorker(version_id_)) {
remote_.reset();
}
}
void ServiceWorkerMain::OnVersionRedundant() {
// Redundant service workers have become either unregistered or replaced.
// A new ServiceWorkerMain will need to be created.
Destroy();
}
bool ServiceWorkerMain::IsDestroyed() const {
return version_destroyed_;
}
const blink::StorageKey ServiceWorkerMain::GetStorageKey() {
GURL scope = version_info()->scope;
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) {
if (version_destroyed_) {
isolate->ThrowException(v8::Exception::TypeError(
gin::StringToV8(isolate, "ServiceWorkerMain is destroyed")));
return;
}
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;
}
service_worker_context_->FinishedExternalRequest(version_id_, request_uuid);
}
size_t ServiceWorkerMain::CountExternalRequests() {
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> ServiceWorkerMain::New(v8::Isolate* isolate) {
return gin::Handle<ServiceWorkerMain>();
}
// static
gin::Handle<ServiceWorkerMain> 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<ServiceWorkerMain>();
}
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<v8::ObjectTemplate> 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::CountExternalRequests)
.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<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> 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)

View File

@@ -0,0 +1,174 @@
// 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 <optional>
#include <string>
#include <vector>
#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 <typename T>
class Handle;
template <typename T>
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<const content::StoragePartition> 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<const content::StoragePartition*>()(
key.storage_partition) ^
std::hash<int64_t>()(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<ServiceWorkerMain>,
public gin_helper::EventEmitterMixin<ServiceWorkerMain>,
public gin_helper::Pinnable<ServiceWorkerMain>,
public gin_helper::Constructible<ServiceWorkerMain> {
public:
// Create a new ServiceWorkerMain and return the V8 wrapper of it.
static gin::Handle<ServiceWorkerMain> New(v8::Isolate* isolate);
static gin::Handle<ServiceWorkerMain> 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<v8::ObjectTemplate>);
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();
void OnVersionRedundant();
protected:
explicit ServiceWorkerMain(content::ServiceWorkerContext* sw_context,
int64_t version_id,
const ServiceWorkerKey& key);
~ServiceWorkerMain() override;
private:
void Destroy();
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 CountExternalRequests();
// 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<v8::Value> 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;
// Store copy of version info so it's accessible when not running.
std::unique_ptr<content::ServiceWorkerVersionBaseInfo> version_info_;
raw_ptr<content::ServiceWorkerContext> service_worker_context_;
mojo::AssociatedRemote<mojom::ElectronRenderer> remote_;
std::unique_ptr<gin_helper::Promise<void>> start_worker_promise_;
base::WeakPtrFactory<ServiceWorkerMain> weak_factory_{this};
};
} // namespace electron::api
#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_SERVICE_WORKER_MAIN_H_

View File

@@ -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<v8::Value> Converter<blink::EmbeddedWorkerStatus>::ToV8(
v8::Isolate* isolate,
const blink::EmbeddedWorkerStatus& val) {
static constexpr auto Lookup =
base::MakeFixedFlatMap<blink::EmbeddedWorkerStatus, std::string_view>({
{blink::EmbeddedWorkerStatus::kStarting, "starting"},
{blink::EmbeddedWorkerStatus::kRunning, "running"},
{blink::EmbeddedWorkerStatus::kStopping, "stopping"},
{blink::EmbeddedWorkerStatus::kStopped, "stopped"},
});
return StringToV8(isolate, Lookup.at(val));
}
} // namespace gin

View File

@@ -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<blink::EmbeddedWorkerStatus> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const blink::EmbeddedWorkerStatus& val);
};
} // namespace gin
#endif // ELECTRON_SHELL_COMMON_GIN_CONVERTERS_SERVICE_WORKER_CONVERTER_H_

View File

@@ -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) \

View File

@@ -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 };

View File

@@ -66,6 +66,19 @@ declare namespace Electron {
}
}
interface ServiceWorkers {
_getWorkerFromVersionIDIfExists(versionId: number): Electron.ServiceWorkerMain | undefined;
_stopAllWorkers(): Promise<void>;
}
interface ServiceWorkerMain {
_send(internal: boolean, channel: string, args: any): void;
_startExternalRequest(hasTimeout: boolean): { id: string, ok: boolean };
_finishExternalRequest(uuid: string): void;
_countExternalRequests(): number;
}
interface TouchBar {
_removeFromWindow: (win: BaseWindow) => void;
}