зеркало из https://github.com/electron/electron.git
feat: ServiceWorkerMain (#45232)
* feat: ServiceWorkerMain * refactor: disconnect remote * handle version_info_ nullptr case * initiate finish request when possible and enumerate errors * explicit name for test method * oops * fix: wait for redundant version to stop before destroying * docs: clarify when undefined is returned * chore: remove extra semicolons
This commit is contained in:
Родитель
75eac86506
Коммит
a467d0684e
|
@ -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)
|
||||
|
|
|
@ -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)<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.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.
|
|
@ -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<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. 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<ServiceWorkerMain>` - Resolves with the service worker when it's started.
|
||||
|
||||
Starts the service worker or does nothing if already running.
|
||||
|
||||
<!-- TODO(samuelmaddock): extend example to send IPC after starting worker -->
|
||||
|
||||
```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)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -14,6 +14,21 @@ This document uses the following convention to categorize breaking changes:
|
|||
|
||||
## Planned Breaking API Changes (35.0)
|
||||
|
||||
### 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: `setPreloads`, `getPreloads` on `Session`
|
||||
|
||||
`registerPreloadScript`, `unregisterPreloadScript`, and `getPreloadScripts` are introduced as a
|
||||
|
@ -21,7 +36,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')])
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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') },
|
||||
|
|
|
@ -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;
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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<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,
|
||||
content::StatusCodeResponse status) {
|
||||
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() {
|
||||
|
|
|
@ -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,
|
||||
content::StatusCodeResponse status);
|
||||
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};
|
||||
};
|
||||
|
||||
|
|
|
@ -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 <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();
|
||||
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<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(
|
||||
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> 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::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<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)
|
|
@ -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 <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(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<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;
|
||||
|
||||
// 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<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_
|
|
@ -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
|
|
@ -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_
|
|
@ -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) \
|
||||
|
|
|
@ -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<Electron.ServiceWorkerMain>((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<void>(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<void>(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}/`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2,7 +2,8 @@
|
|||
<html lang="en">
|
||||
<body>
|
||||
<script>
|
||||
navigator.serviceWorker.register('sw.js', {
|
||||
let scriptUrl = new URLSearchParams(location.search).get('scriptUrl') || 'sw.js';
|
||||
navigator.serviceWorker.register(scriptUrl, {
|
||||
scope: location.pathname.split('/').slice(0, 2).join('/') + '/'
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
throw new Error('service worker throwing on startup');
|
|
@ -0,0 +1,3 @@
|
|||
self.addEventListener('install', function () {
|
||||
registration.unregister();
|
||||
});
|
|
@ -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 };
|
||||
|
|
|
@ -66,6 +66,18 @@ declare namespace Electron {
|
|||
}
|
||||
}
|
||||
|
||||
interface ServiceWorkers {
|
||||
_getWorkerFromVersionIDIfExists(versionId: number): Electron.ServiceWorkerMain | undefined;
|
||||
_stopAllWorkers(): Promise<void>;
|
||||
}
|
||||
|
||||
interface ServiceWorkerMain {
|
||||
_startExternalRequest(hasTimeout: boolean): { id: string, ok: boolean };
|
||||
_finishExternalRequest(uuid: string): void;
|
||||
_countExternalRequests(): number;
|
||||
}
|
||||
|
||||
|
||||
interface TouchBar {
|
||||
_removeFromWindow: (win: BaseWindow) => void;
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче