mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
Close the outside port and remove its message forwarding listeners synchronously. Closing the port alone is asynchronous and can leave queued messages dispatching when terminate() runs inside a listener. Allow the current event to finish while discarding subsequent messages, including those drained when the backing thread exits. Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66354 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Aviv Keller <me@aviv.sh>
1014 lines
33 KiB
JavaScript
1014 lines
33 KiB
JavaScript
'use strict';
|
|
|
|
// The Web IDL and specification prose quoted below is reproduced verbatim
|
|
// from the specification, including its original (lowercase) casing.
|
|
// Refs: https://html.spec.whatwg.org/multipage/workers.html
|
|
/* eslint-disable capitalized-comments */
|
|
|
|
const {
|
|
ArrayPrototypePush,
|
|
FunctionPrototypeBind,
|
|
ObjectDefineProperties,
|
|
ObjectDefineProperty,
|
|
ObjectGetPrototypeOf,
|
|
ObjectSetPrototypeOf,
|
|
ReflectDeleteProperty,
|
|
RegExpPrototypeExec,
|
|
SafeSet,
|
|
StringPrototypeSlice,
|
|
Symbol,
|
|
SymbolFor,
|
|
SymbolIterator,
|
|
SymbolToStringTag,
|
|
globalThis,
|
|
} = primordials;
|
|
|
|
const { extname } = require('path');
|
|
|
|
const {
|
|
ERR_ILLEGAL_CONSTRUCTOR,
|
|
ERR_INVALID_STATE,
|
|
ERR_NO_CRYPTO,
|
|
} = require('internal/errors').codes;
|
|
|
|
const {
|
|
EventTarget,
|
|
defineEventHandler,
|
|
initEventTarget,
|
|
} = require('internal/event_target');
|
|
|
|
const { getOptionValue } = require('internal/options');
|
|
|
|
const {
|
|
assignFunctionName,
|
|
defineOperation,
|
|
exposeInterface,
|
|
getCWDURL,
|
|
getLazy,
|
|
kEmptyObject,
|
|
kEnumerableProperty,
|
|
lazyDOMException,
|
|
} = require('internal/util');
|
|
|
|
const {
|
|
validateThisInternalField,
|
|
} = require('internal/validators');
|
|
|
|
const {
|
|
converters,
|
|
createDictionaryConverter,
|
|
createEnumConverter,
|
|
requiredArguments,
|
|
} = require('internal/webidl');
|
|
|
|
const {
|
|
Worker: NodeWorker,
|
|
kPublicPort,
|
|
kWebWorkerData,
|
|
} = require('internal/worker');
|
|
|
|
const {
|
|
MessageChannel,
|
|
lazyMessageEvent,
|
|
} = require('internal/worker/io');
|
|
|
|
const {
|
|
vm_dynamic_import_default_internal,
|
|
} = internalBinding('symbols');
|
|
|
|
const {
|
|
hasOpenSSL,
|
|
} = internalBinding('config');
|
|
|
|
const {
|
|
Navigator,
|
|
kInitialize: kInitializeNavigator,
|
|
} = require('internal/navigator');
|
|
|
|
const {
|
|
URL,
|
|
URLParse,
|
|
fileURLToPath,
|
|
} = require('internal/url');
|
|
|
|
const {
|
|
kType: kBlobType,
|
|
getBlobDataSync,
|
|
resolveObjectURL,
|
|
} = require('internal/blob');
|
|
|
|
const kCurrentlyReceivingPorts =
|
|
SymbolFor('nodejs.internal.kCurrentlyReceivingPorts');
|
|
const kRef = SymbolFor('nodejs.ref');
|
|
const kUnref = SymbolFor('nodejs.unref');
|
|
|
|
const kCreate = Symbol('kCreate');
|
|
const kInsidePort = Symbol('kInsidePort');
|
|
const kLocation = Symbol('kLocation');
|
|
const kName = Symbol('kName');
|
|
const kNavigator = Symbol('kNavigator');
|
|
const kNavigatorBrand = Symbol('kNavigatorBrand');
|
|
const kOutsidePort = Symbol('kOutsidePort');
|
|
const kRemoveMessageListeners = Symbol('kRemoveMessageListeners');
|
|
const kType = Symbol('kType');
|
|
const kURL = Symbol('kURL');
|
|
const kWorker = Symbol('kWorker');
|
|
|
|
let scopeBaseURL = null;
|
|
|
|
const lazyErrorEvent =
|
|
getLazy(() => require('internal/deps/undici/undici').ErrorEvent);
|
|
|
|
const lazyStripTypeScriptModuleTypes = getLazy(
|
|
() => require('internal/modules/typescript').stripTypeScriptModuleTypes);
|
|
|
|
function createErrorEvent(init) {
|
|
const ErrorEvent = lazyErrorEvent();
|
|
return new ErrorEvent('error', init);
|
|
}
|
|
|
|
// "Fire an event named message at messageEventTarget, using MessageEvent,
|
|
// with the data attribute initialized to messageClone and the ports
|
|
// attribute initialized to newPorts."
|
|
//
|
|
// "If this throws an exception, catch it, fire an event named messageerror
|
|
// at messageEventTarget, using MessageEvent, and then return."
|
|
function forwardMessageEvents(port, target) {
|
|
const onMessage = (data) =>
|
|
target.dispatchEvent(lazyMessageEvent('message', {
|
|
data,
|
|
ports: port[kCurrentlyReceivingPorts],
|
|
}));
|
|
const onMessageError = (data) =>
|
|
target.dispatchEvent(lazyMessageEvent('messageerror', {
|
|
data,
|
|
}));
|
|
port.on('message', onMessage);
|
|
port.on('messageerror', onMessageError);
|
|
return () => {
|
|
port.removeListener('message', onMessage);
|
|
port.removeListener('messageerror', onMessageError);
|
|
};
|
|
}
|
|
|
|
// Web IDL [Replaceable]: the setter replaces the accessor with an own
|
|
// data property on the receiver.
|
|
// Refs: https://webidl.spec.whatwg.org/#Replaceable
|
|
function replaceAttribute(receiver, key, value) {
|
|
ObjectDefineProperty(receiver, key, {
|
|
__proto__: null,
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value,
|
|
});
|
|
}
|
|
|
|
// https://mimesniff.spec.whatwg.org/#javascript-mime-type
|
|
const kJavaScriptMIMETypes = new SafeSet([
|
|
'application/ecmascript',
|
|
'application/javascript',
|
|
'application/x-ecmascript',
|
|
'application/x-javascript',
|
|
'text/ecmascript',
|
|
'text/javascript',
|
|
'text/javascript1.0',
|
|
'text/javascript1.1',
|
|
'text/javascript1.2',
|
|
'text/javascript1.3',
|
|
'text/javascript1.4',
|
|
'text/javascript1.5',
|
|
'text/jscript',
|
|
'text/livescript',
|
|
'text/x-ecmascript',
|
|
'text/x-javascript',
|
|
]);
|
|
|
|
function isJavaScriptMIMEType(essence) {
|
|
return kJavaScriptMIMETypes.has(essence);
|
|
}
|
|
|
|
// Extracts the essence ("type/subtype") of a raw MIME type string, or
|
|
// returns undefined if the string is not a parsable MIME type.
|
|
function mimeTypeEssence(type) {
|
|
const { MIMEType } = require('internal/mime');
|
|
try {
|
|
return new MIMEType(type).essence;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function utf8Decode(bytes) {
|
|
return require('internal/encoding').getUtf8Decoder().decode(bytes);
|
|
}
|
|
|
|
// Reads and decodes the source text of a JavaScript data: URL, or returns
|
|
// undefined if the URL fails to parse as a data: URL or its MIME type is
|
|
// not a JavaScript MIME type.
|
|
function readDataURLScript(url) {
|
|
const { dataURLProcessor } = require('internal/data_url');
|
|
const data = dataURLProcessor(url);
|
|
if (data === 'failure' || !isJavaScriptMIMEType(data.mimeType.essence)) {
|
|
return undefined;
|
|
}
|
|
return utf8Decode(data.body);
|
|
}
|
|
|
|
// Reads the data of a blob whose contents are to be run as a script, or
|
|
// returns undefined if the blob's MIME type is not a JavaScript MIME type
|
|
// or its data is not synchronously available.
|
|
function getScriptBlobData(blob, allowEmptyType = false) {
|
|
if (blob === undefined ||
|
|
(!(allowEmptyType && blob[kBlobType] === '') &&
|
|
!isJavaScriptMIMEType(mimeTypeEssence(blob[kBlobType])))) {
|
|
return undefined;
|
|
}
|
|
return getBlobDataSync(blob);
|
|
}
|
|
|
|
/**
|
|
* Runs the source text of a classic script in the current global scope,
|
|
* emulating "run a classic script". Dynamic import() from the script is
|
|
* resolved by the ESM loader relative to the script's URL.
|
|
* @param {string} source The source text of the script.
|
|
* @param {string} url The URL of the script, used as its filename and as
|
|
* the base URL for dynamic imports.
|
|
* @returns {any}
|
|
*/
|
|
function runClassicScriptSource(source, url) {
|
|
const {
|
|
makeContextifyScript,
|
|
runScriptInThisContext,
|
|
} = require('internal/vm');
|
|
const script = makeContextifyScript(
|
|
source, // code
|
|
url, // filename
|
|
0, // lineOffset
|
|
0, // columnOffset
|
|
undefined, // cachedData
|
|
false, // produceCachedData
|
|
undefined, // parsingContext
|
|
vm_dynamic_import_default_internal, // hostDefinedOptionId
|
|
vm_dynamic_import_default_internal, // importModuleDynamically
|
|
);
|
|
// "Run the classic script script, with rethrow errors set to true."
|
|
return runScriptInThisContext(script, true, false);
|
|
}
|
|
|
|
/**
|
|
* Runs the source text of a module script in the current global scope.
|
|
* @param {string} source The source text of the script.
|
|
* @param {string} url The URL of the script, used as its identifier and as
|
|
* the base URL for imports.
|
|
* @returns {Promise}
|
|
*/
|
|
function runModuleScriptSource(source, url) {
|
|
return require('internal/modules/run_main').runEntryPointWithESMLoader(
|
|
(loader) => {
|
|
const parsedURL = new URL(url);
|
|
if (parsedURL.protocol === 'file:' && getOptionValue('--strip-types')) {
|
|
const filename = fileURLToPath(parsedURL);
|
|
const extension = extname(filename);
|
|
if (extension === '.ts' || extension === '.mts' || extension === '.cts') {
|
|
source = lazyStripTypeScriptModuleTypes()(source, filename, url);
|
|
}
|
|
}
|
|
// Necessary to reset RegExp statics before user code runs.
|
|
RegExpPrototypeExec(/^/, '');
|
|
return loader.eval(source, url, true);
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Synchronously obtains the source text of a classic script, emulating
|
|
* "fetch a classic worker-imported script" for locally resolvable URLs.
|
|
* @param {URL} url The parsed URL of the script.
|
|
* @param {Blob|undefined} blob The blob URL entry captured when url was
|
|
* parsed, for blob: URLs.
|
|
* @returns {string}
|
|
*/
|
|
function fetchClassicScriptSourceSync(url, blob) {
|
|
switch (url.protocol) {
|
|
case 'file:':
|
|
try {
|
|
return require('fs').readFileSync(url, 'utf8');
|
|
} catch {
|
|
break;
|
|
}
|
|
case 'data:': {
|
|
const source = readDataURLScript(url);
|
|
if (source !== undefined) {
|
|
return source;
|
|
}
|
|
break;
|
|
}
|
|
case 'blob:': {
|
|
const data = getScriptBlobData(blob);
|
|
if (data !== undefined) {
|
|
return utf8Decode(data);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
// "If response's ... or its Content-Type metadata is not a JavaScript
|
|
// MIME type, then throw a "NetworkError" DOMException."
|
|
throw lazyDOMException(`Failed to load script: ${url.href}`, 'NetworkError');
|
|
}
|
|
|
|
// Web IDL overload resolution for the postMessage(message, transfer) /
|
|
// postMessage(message, options) overload pair: iterable objects select the
|
|
// sequence<object> overload, everything else is converted as a
|
|
// StructuredSerializeOptions dictionary.
|
|
function normalizeTransfer(transferOrOptions, options) {
|
|
if ((typeof transferOrOptions === 'object' && transferOrOptions !== null) ||
|
|
typeof transferOrOptions === 'function') {
|
|
const method = transferOrOptions[SymbolIterator];
|
|
if (method != null) {
|
|
return converters['sequence<object>'](transferOrOptions, options, method);
|
|
}
|
|
}
|
|
return converters.StructuredSerializeOptions(transferOrOptions, options)
|
|
.transfer;
|
|
}
|
|
|
|
const kScopePostMessageOptions = {
|
|
__proto__: null,
|
|
prefix: "Failed to execute 'postMessage' on 'DedicatedWorkerGlobalScope'",
|
|
context: 'Argument 2',
|
|
};
|
|
|
|
const kWorkerPostMessageOptions = {
|
|
__proto__: null,
|
|
prefix: "Failed to execute 'postMessage' on 'Worker'",
|
|
context: 'Argument 2',
|
|
};
|
|
|
|
// https://html.spec.whatwg.org/multipage/workers.html#the-workernavigator-interface
|
|
//
|
|
// [Exposed=Worker]
|
|
// interface WorkerNavigator {};
|
|
//
|
|
// The NavigatorLanguage, NavigatorConcurrentHardware and NavigatorLocks
|
|
// mixins, as well as userAgent and platform of NavigatorID, are inherited
|
|
// from the Node.js Navigator implementation; the members below are the
|
|
// remainder of the mixins WorkerNavigator includes.
|
|
class WorkerNavigator extends Navigator {
|
|
constructor(guard = undefined) {
|
|
if (guard !== kCreate)
|
|
throw new ERR_ILLEGAL_CONSTRUCTOR();
|
|
super(kInitializeNavigator);
|
|
this[kNavigatorBrand] = true;
|
|
}
|
|
|
|
// WorkerNavigator includes NavigatorID;
|
|
// Refs: https://html.spec.whatwg.org/multipage/system-state.html#navigatorid
|
|
//
|
|
// interface mixin NavigatorID {
|
|
// readonly attribute DOMString appVersion;
|
|
get appVersion() {
|
|
validateThisInternalField(this, kNavigatorBrand, 'WorkerNavigator');
|
|
return StringPrototypeSlice(process.version, 1);
|
|
}
|
|
// };
|
|
|
|
// WorkerNavigator includes NavigatorLocks;
|
|
get locks() {
|
|
return super.locks;
|
|
}
|
|
}
|
|
|
|
// Refs: https://html.spec.whatwg.org/multipage/system-state.html#navigatoronline
|
|
//
|
|
// interface mixin NavigatorOnLine {
|
|
// readonly attribute boolean onLine;
|
|
// "Returns false if the user agent is definitely offline. Returns true
|
|
// if the user agent might be online."
|
|
// };
|
|
const kNavigatorConstants = [
|
|
['appCodeName', 'Mozilla'],
|
|
['appName', 'Netscape'],
|
|
['product', 'Gecko'],
|
|
['onLine', true],
|
|
];
|
|
for (let i = 0; i < kNavigatorConstants.length; i++) {
|
|
const { 0: key, 1: value } = kNavigatorConstants[i];
|
|
ObjectDefineProperty(WorkerNavigator.prototype, key, {
|
|
__proto__: null,
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: assignFunctionName(`get ${key}`, function() {
|
|
validateThisInternalField(this, kNavigatorBrand, 'WorkerNavigator');
|
|
return value;
|
|
}),
|
|
});
|
|
}
|
|
|
|
ObjectDefineProperties(WorkerNavigator.prototype, {
|
|
appVersion: kEnumerableProperty,
|
|
locks: kEnumerableProperty,
|
|
[SymbolToStringTag]: {
|
|
__proto__: null,
|
|
configurable: true,
|
|
value: 'WorkerNavigator',
|
|
},
|
|
});
|
|
|
|
// https://html.spec.whatwg.org/multipage/workers.html#worker-locations
|
|
//
|
|
// [Exposed=Worker]
|
|
// interface WorkerLocation {
|
|
class WorkerLocation {
|
|
constructor(guard = undefined, url = undefined) {
|
|
if (guard !== kCreate)
|
|
throw new ERR_ILLEGAL_CONSTRUCTOR();
|
|
// "A WorkerLocation object has an associated WorkerGlobalScope object
|
|
// (a WorkerGlobalScope object)."
|
|
this[kURL] = url;
|
|
}
|
|
|
|
// "stringifier", for the href attribute defined below
|
|
toString() {
|
|
validateThisInternalField(this, kURL, 'WorkerLocation');
|
|
return this[kURL].href;
|
|
}
|
|
}
|
|
// };
|
|
|
|
// stringifier readonly attribute USVString href;
|
|
// readonly attribute USVString origin;
|
|
// readonly attribute USVString protocol;
|
|
// readonly attribute USVString host;
|
|
// readonly attribute USVString hostname;
|
|
// readonly attribute USVString port;
|
|
// readonly attribute USVString pathname;
|
|
// readonly attribute USVString search;
|
|
// readonly attribute USVString hash;
|
|
const kWorkerLocationAttributes = [
|
|
'href', 'origin', 'protocol', 'host', 'hostname', 'port', 'pathname',
|
|
'search', 'hash',
|
|
];
|
|
for (let i = 0; i < kWorkerLocationAttributes.length; i++) {
|
|
const key = kWorkerLocationAttributes[i];
|
|
ObjectDefineProperty(WorkerLocation.prototype, key, {
|
|
__proto__: null,
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: assignFunctionName(`get ${key}`, function() {
|
|
validateThisInternalField(this, kURL, 'WorkerLocation');
|
|
return this[kURL][key];
|
|
}),
|
|
});
|
|
}
|
|
|
|
ObjectDefineProperties(WorkerLocation.prototype, {
|
|
toString: kEnumerableProperty,
|
|
[SymbolToStringTag]: {
|
|
__proto__: null,
|
|
configurable: true,
|
|
value: 'WorkerLocation',
|
|
},
|
|
});
|
|
|
|
// https://html.spec.whatwg.org/multipage/workers.html#the-workerglobalscope-common-interface
|
|
//
|
|
// [Exposed=Worker]
|
|
// interface WorkerGlobalScope : EventTarget {
|
|
class WorkerGlobalScope extends EventTarget {
|
|
constructor(guard = undefined) {
|
|
if (guard !== kCreate)
|
|
throw new ERR_ILLEGAL_CONSTRUCTOR();
|
|
super();
|
|
}
|
|
|
|
// readonly attribute WorkerGlobalScope self;
|
|
get self() {
|
|
validateThisInternalField(this, kLocation, 'WorkerGlobalScope');
|
|
// "The self attribute must return the WorkerGlobalScope object itself."
|
|
return this;
|
|
}
|
|
|
|
// readonly attribute WorkerLocation location;
|
|
get location() {
|
|
validateThisInternalField(this, kLocation, 'WorkerGlobalScope');
|
|
// "The location attribute must return the WorkerLocation object whose
|
|
// associated WorkerGlobalScope object is the WorkerGlobalScope object."
|
|
return this[kLocation];
|
|
}
|
|
|
|
// readonly attribute WorkerNavigator navigator;
|
|
get navigator() {
|
|
validateThisInternalField(this, kLocation, 'WorkerGlobalScope');
|
|
// "The navigator attribute of the WorkerGlobalScope interface must
|
|
// return an instance of the WorkerNavigator interface, which represents
|
|
// the identity and state of the user agent (the client)."
|
|
return this[kNavigator] ??= new WorkerNavigator(kCreate);
|
|
}
|
|
|
|
// undefined importScripts((TrustedScriptURL or USVString)... urls);
|
|
importScripts(...urls) {
|
|
validateThisInternalField(this, kLocation, 'WorkerGlobalScope');
|
|
const prefix = "Failed to execute 'importScripts' on 'WorkerGlobalScope'";
|
|
// Web IDL converts every argument before running the method steps.
|
|
for (let i = 0; i < urls.length; i++) {
|
|
urls[i] = converters.USVString(
|
|
urls[i], { prefix, context: `Argument ${i + 1}` });
|
|
}
|
|
// "To import scripts into worker global scope, given a
|
|
// WorkerGlobalScope object worker global scope, a list of scalar value
|
|
// strings urls, and an optional perform the fetch hook performFetch:"
|
|
//
|
|
// "If worker global scope's type is "module", throw a TypeError
|
|
// exception."
|
|
if (this[kType] === 'module') {
|
|
throw new ERR_INVALID_STATE.TypeError(
|
|
'importScripts() cannot be used in a module worker');
|
|
}
|
|
// "If urls is empty, return."
|
|
if (urls.length === 0)
|
|
return;
|
|
|
|
// "Let urlRecords be [an empty list]"
|
|
const urlRecords = [];
|
|
// "For each url of urls:"
|
|
for (let i = 0; i < urls.length; i++) {
|
|
const url = urls[i];
|
|
// "Let urlRecord be the result of encoding-parsing a URL given url,
|
|
// relative to settings object."
|
|
const urlRecord = URLParse(url, this[kLocation].href);
|
|
|
|
if (urlRecord === null) {
|
|
// "If urlRecord is failure, then throw a "SyntaxError"
|
|
// DOMException."
|
|
throw lazyDOMException(`Failed to parse URL: ${url}`, 'SyntaxError');
|
|
}
|
|
// Refs: https://url.spec.whatwg.org/#concept-url-parser
|
|
const blob = urlRecord.protocol === 'blob:' ?
|
|
resolveObjectURL(urlRecord.href) : undefined;
|
|
// "Append urlRecord to urlRecords."
|
|
ArrayPrototypePush(urlRecords, { url: urlRecord, blob });
|
|
}
|
|
// "For each urlRecord of urlRecords:"
|
|
for (let i = 0; i < urlRecords.length; i++) {
|
|
// "Fetch a classic worker-imported script given urlRecord and
|
|
// settings object, passing along performFetch if provided. If this
|
|
// succeeds, let script be the result. Otherwise, rethrow the
|
|
// exception."
|
|
const { url, blob } = urlRecords[i];
|
|
const script = fetchClassicScriptSourceSync(url, blob);
|
|
// "Run the classic script script, with rethrow errors set to true."
|
|
runClassicScriptSource(script, url.href);
|
|
}
|
|
}
|
|
}
|
|
|
|
ObjectDefineProperties(WorkerGlobalScope.prototype, {
|
|
self: kEnumerableProperty,
|
|
location: kEnumerableProperty,
|
|
navigator: kEnumerableProperty,
|
|
importScripts: kEnumerableProperty,
|
|
[SymbolToStringTag]: {
|
|
__proto__: null,
|
|
configurable: true,
|
|
value: 'WorkerGlobalScope',
|
|
},
|
|
});
|
|
|
|
// attribute OnErrorEventHandler onerror;
|
|
defineEventHandler(WorkerGlobalScope.prototype, 'error');
|
|
// attribute EventHandler onlanguagechange;
|
|
defineEventHandler(WorkerGlobalScope.prototype, 'languagechange');
|
|
// attribute EventHandler onoffline;
|
|
defineEventHandler(WorkerGlobalScope.prototype, 'offline');
|
|
// attribute EventHandler ononline;
|
|
defineEventHandler(WorkerGlobalScope.prototype, 'online');
|
|
// attribute EventHandler onrejectionhandled;
|
|
defineEventHandler(WorkerGlobalScope.prototype, 'rejectionhandled');
|
|
// attribute EventHandler onunhandledrejection;
|
|
defineEventHandler(WorkerGlobalScope.prototype, 'unhandledrejection');
|
|
// };
|
|
|
|
// https://html.spec.whatwg.org/multipage/webappapis.html#windoworworkerglobalscope
|
|
//
|
|
// WorkerGlobalScope includes WindowOrWorkerGlobalScope;
|
|
|
|
// https://w3c.github.io/webcrypto/#crypto-interface
|
|
//
|
|
// partial interface mixin WindowOrWorkerGlobalScope {
|
|
// [SameObject] readonly attribute Crypto crypto;
|
|
// };
|
|
const lazyCrypto = getLazy(() => require('internal/crypto/webcrypto').crypto);
|
|
ObjectDefineProperty(WorkerGlobalScope.prototype, 'crypto', {
|
|
__proto__: null,
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: assignFunctionName('get crypto', function() {
|
|
validateThisInternalField(this, kLocation, 'WorkerGlobalScope');
|
|
if (!hasOpenSSL)
|
|
throw new ERR_NO_CRYPTO();
|
|
return lazyCrypto();
|
|
}),
|
|
});
|
|
|
|
// https://w3c.github.io/hr-time/#the-performance-attribute
|
|
//
|
|
// partial interface mixin WindowOrWorkerGlobalScope {
|
|
// [Replaceable] readonly attribute Performance performance;
|
|
// };
|
|
const lazyPerformance = getLazy(() => require('perf_hooks').performance);
|
|
ObjectDefineProperty(WorkerGlobalScope.prototype, 'performance', {
|
|
__proto__: null,
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: assignFunctionName('get performance', function() {
|
|
validateThisInternalField(this, kLocation, 'WorkerGlobalScope');
|
|
return lazyPerformance();
|
|
}),
|
|
set: assignFunctionName('set performance', function(value) {
|
|
validateThisInternalField(this, kLocation, 'WorkerGlobalScope');
|
|
replaceAttribute(this, 'performance', value);
|
|
}),
|
|
});
|
|
|
|
// Refs: https://html.spec.whatwg.org/multipage/web-messaging.html#messageeventtarget
|
|
//
|
|
// interface mixin MessageEventTarget {
|
|
function includeMessageEventTarget(prototype) {
|
|
// attribute EventHandler onmessage;
|
|
defineEventHandler(prototype, 'message');
|
|
// attribute EventHandler onmessageerror;
|
|
defineEventHandler(prototype, 'messageerror');
|
|
}
|
|
// };
|
|
|
|
// https://html.spec.whatwg.org/multipage/workers.html#dedicated-workers-and-the-dedicatedworkerglobalscope-interface
|
|
//
|
|
// [Global=(Worker,DedicatedWorker),Exposed=DedicatedWorker]
|
|
// interface DedicatedWorkerGlobalScope : WorkerGlobalScope {
|
|
class DedicatedWorkerGlobalScope extends WorkerGlobalScope {
|
|
// [Replaceable] readonly attribute DOMString name;
|
|
get name() {
|
|
validateThisInternalField(this, kInsidePort, 'DedicatedWorkerGlobalScope');
|
|
// "The name getter steps are to return this's name. Its value
|
|
// represents the name given to the worker using the Worker constructor,
|
|
// used primarily for debugging purposes."
|
|
return this[kName];
|
|
}
|
|
|
|
set name(value) {
|
|
validateThisInternalField(this, kInsidePort, 'DedicatedWorkerGlobalScope');
|
|
replaceAttribute(this, 'name', value);
|
|
}
|
|
|
|
// undefined postMessage(any message, sequence<object> transfer);
|
|
// undefined postMessage(any message, optional StructuredSerializeOptions options = {});
|
|
postMessage(message, transferOrOptions = kEmptyObject) {
|
|
validateThisInternalField(this, kInsidePort, 'DedicatedWorkerGlobalScope');
|
|
requiredArguments(arguments.length, 1, kScopePostMessageOptions);
|
|
// When the second argument was omitted there is nothing to convert.
|
|
const transfer = transferOrOptions === kEmptyObject ? undefined :
|
|
normalizeTransfer(transferOrOptions, kScopePostMessageOptions);
|
|
// "The postMessage(message, transfer) and postMessage(message, options)
|
|
// methods on DedicatedWorkerGlobalScope objects act as if, when
|
|
// invoked, it immediately invoked the respective postMessage(message,
|
|
// transfer) and postMessage(message, options) on the port, with the
|
|
// same arguments, and returned the same return value."
|
|
this[kInsidePort]?.postMessage(message, transfer);
|
|
}
|
|
|
|
// undefined close();
|
|
close() {
|
|
validateThisInternalField(this, kInsidePort, 'DedicatedWorkerGlobalScope');
|
|
// "The close() method steps are to close a worker given this."
|
|
//
|
|
// "To close a worker, given a workerGlobal, run these steps:
|
|
// 1. Discard any tasks that have been added to workerGlobal's relevant
|
|
// agent's event loop's task queues.
|
|
// 2. Set workerGlobal's closing flag to true. (This prevents any
|
|
// further tasks from being queued.)"
|
|
process.exit();
|
|
}
|
|
}
|
|
|
|
ObjectDefineProperties(DedicatedWorkerGlobalScope.prototype, {
|
|
name: kEnumerableProperty,
|
|
postMessage: kEnumerableProperty,
|
|
close: kEnumerableProperty,
|
|
[SymbolToStringTag]: {
|
|
__proto__: null,
|
|
configurable: true,
|
|
value: 'DedicatedWorkerGlobalScope',
|
|
},
|
|
});
|
|
// };
|
|
|
|
// DedicatedWorkerGlobalScope includes MessageEventTarget;
|
|
includeMessageEventTarget(DedicatedWorkerGlobalScope.prototype);
|
|
|
|
// https://html.spec.whatwg.org/multipage/workers.html#abstractworker
|
|
//
|
|
// interface mixin AbstractWorker {
|
|
function includeAbstractWorker(prototype) {
|
|
// attribute EventHandler onerror;
|
|
defineEventHandler(prototype, 'error');
|
|
}
|
|
// };
|
|
|
|
// https://html.spec.whatwg.org/multipage/workers.html#worker-options
|
|
//
|
|
// enum WorkerType { "classic", "module" };
|
|
const convertWorkerType = createEnumConverter('WorkerType', [
|
|
'classic',
|
|
'module',
|
|
]);
|
|
|
|
// RequestCredentials is defined by the Fetch Standard.
|
|
// Refs: https://fetch.spec.whatwg.org/#requestcredentials
|
|
//
|
|
// enum RequestCredentials { "omit", "same-origin", "include" };
|
|
const convertRequestCredentials = createEnumConverter('RequestCredentials', [
|
|
'omit',
|
|
'same-origin',
|
|
'include',
|
|
]);
|
|
|
|
// dictionary WorkerOptions {
|
|
const convertWorkerOptions = createDictionaryConverter(
|
|
'WorkerOptions', [
|
|
{
|
|
// DOMString name = "";
|
|
key: 'name',
|
|
converter: converters.DOMString,
|
|
defaultValue: () => '',
|
|
},
|
|
{
|
|
// WorkerType type = "classic";
|
|
key: 'type',
|
|
converter: convertWorkerType,
|
|
defaultValue: () => 'classic',
|
|
},
|
|
{
|
|
// RequestCredentials credentials = "same-origin"; // credentials is only used if type is "module"
|
|
// (We don't actually use this, but parity with the spec is good, right?)
|
|
key: 'credentials',
|
|
converter: convertRequestCredentials,
|
|
defaultValue: () => 'same-origin',
|
|
},
|
|
]);
|
|
// };
|
|
|
|
/**
|
|
* @param {URL} workerURL The parsed URL of the worker script.
|
|
* @returns {{ value?: URL, source?: string }|null}
|
|
*/
|
|
function resolveWorkerEntry(workerURL) {
|
|
switch (workerURL.protocol) {
|
|
case 'file:':
|
|
return { value: workerURL };
|
|
case 'data:': {
|
|
const source = readDataURLScript(workerURL);
|
|
if (source === undefined) {
|
|
return null;
|
|
}
|
|
return { source };
|
|
}
|
|
case 'blob:': {
|
|
const blob = resolveObjectURL(workerURL.href);
|
|
// Blobs are commonly created without an explicit content type, so an
|
|
// empty type is accepted in addition to JavaScript MIME types.
|
|
const data = getScriptBlobData(blob, true);
|
|
if (data === undefined) {
|
|
return null;
|
|
}
|
|
return { source: utf8Decode(data) };
|
|
}
|
|
default:
|
|
throw lazyDOMException(
|
|
`Worker scripts must be file:, data: or blob: URLs: ${workerURL.href}`,
|
|
'NotSupportedError');
|
|
}
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/workers.html#dedicated-workers-and-the-worker-interface
|
|
//
|
|
// [Exposed=(Window,DedicatedWorker,SharedWorker)]
|
|
// interface Worker : EventTarget {
|
|
class Worker extends EventTarget {
|
|
// constructor((TrustedScriptURL or USVString) scriptURL, optional WorkerOptions options = {});
|
|
constructor(scriptURL, options = kEmptyObject) {
|
|
const prefix = "Failed to construct 'Worker'";
|
|
requiredArguments(arguments.length, 1, { prefix });
|
|
super();
|
|
scriptURL = converters.USVString(
|
|
scriptURL, { prefix, context: 'Argument 1' });
|
|
options = convertWorkerOptions(options, { prefix, context: 'Argument 2' });
|
|
|
|
// "Let workerURL be the result of encoding-parsing a URL given
|
|
// compliantScriptURL, relative to outsideSettings."
|
|
const workerURL = URLParse(scriptURL, scopeBaseURL ?? getCWDURL());
|
|
|
|
if (workerURL === null) {
|
|
// "If workerURL is failure, then throw a "SyntaxError" DOMException."
|
|
throw lazyDOMException(
|
|
`Failed to parse URL: ${scriptURL}`, 'SyntaxError');
|
|
}
|
|
|
|
this[kWorker] = null;
|
|
const entry = resolveWorkerEntry(workerURL);
|
|
if (entry === null) {
|
|
// Even a failed worker has an outside port. Keep a closed port so
|
|
// postMessage() still serializes its message and transfers objects.
|
|
const { port1, port2 } = new MessageChannel();
|
|
port1.close();
|
|
port2.close();
|
|
this[kOutsidePort] = port1;
|
|
// "If the algorithm asynchronously completes with null or with a
|
|
// script whose error to rethrow is non-null, then: Queue a global
|
|
// task on the DOM manipulation task source given worker's relevant
|
|
// global object to fire an event named error at worker."
|
|
const { setImmediate } = require('timers');
|
|
setImmediate(() => {
|
|
this.dispatchEvent(createErrorEvent({
|
|
message: `Failed to fetch the worker script: ${workerURL.href}`,
|
|
cancelable: true,
|
|
}));
|
|
});
|
|
return;
|
|
}
|
|
|
|
// "Run this step in parallel:"
|
|
//
|
|
// "Run a worker given worker, workerURL, outsideSettings, outsidePort,
|
|
// and options."
|
|
this[kWorker] = new NodeWorker(entry.value ?? '', {
|
|
name: options.name,
|
|
eval: entry.source !== undefined,
|
|
[kWebWorkerData]: {
|
|
url: workerURL.href,
|
|
name: options.name,
|
|
type: options.type,
|
|
source: entry.source,
|
|
},
|
|
});
|
|
|
|
this[kOutsidePort] = this[kWorker][kPublicPort];
|
|
this[kRemoveMessageListeners] = forwardMessageEvents(this[kOutsidePort], this);
|
|
|
|
// "Set notHandled to the result of firing an event named error at
|
|
// workerObject, using ErrorEvent, with the cancelable attribute
|
|
// initialized to true, and additional attributes initialized according
|
|
// to errorInfo."
|
|
this[kWorker].on('error', (error) =>
|
|
this.dispatchEvent(createErrorEvent({
|
|
message: `${error?.message ?? error}`,
|
|
error,
|
|
cancelable: true,
|
|
})));
|
|
}
|
|
|
|
// undefined terminate();
|
|
terminate() {
|
|
validateThisInternalField(this, kWorker, 'Worker');
|
|
// "The terminate() method steps are to terminate a worker given this's
|
|
// worker."
|
|
// Closing a port is asynchronous. Remove its forwarding listeners too,
|
|
// since terminate() can run while the backing thread drains queued messages.
|
|
this[kRemoveMessageListeners]?.();
|
|
this[kOutsidePort].close();
|
|
this[kWorker]?.terminate();
|
|
}
|
|
|
|
// undefined postMessage(any message, sequence<object> transfer);
|
|
// undefined postMessage(any message, optional StructuredSerializeOptions options = {});
|
|
postMessage(message, transferOrOptions = kEmptyObject) {
|
|
validateThisInternalField(this, kWorker, 'Worker');
|
|
requiredArguments(arguments.length, 1, kWorkerPostMessageOptions);
|
|
// When the second argument was omitted there is nothing to convert.
|
|
const transfer = transferOrOptions === kEmptyObject ? undefined :
|
|
normalizeTransfer(transferOrOptions, kWorkerPostMessageOptions);
|
|
// "The postMessage(message, transfer) and postMessage(message, options)
|
|
// methods on Worker objects act as if, when invoked, they immediately
|
|
// invoked the respective postMessage(message, transfer) and
|
|
// postMessage(message, options) on this's outside port, with the same
|
|
// arguments, and returned the same return value."
|
|
this[kOutsidePort].postMessage(message, transfer);
|
|
}
|
|
|
|
// The following properties are non-standard, Node.js extensions
|
|
[kRef]() {
|
|
validateThisInternalField(this, kWorker, 'Worker');
|
|
this[kWorker]?.ref();
|
|
}
|
|
|
|
[kUnref]() {
|
|
validateThisInternalField(this, kWorker, 'Worker');
|
|
this[kWorker]?.unref();
|
|
}
|
|
}
|
|
|
|
ObjectDefineProperties(Worker.prototype, {
|
|
terminate: kEnumerableProperty,
|
|
postMessage: kEnumerableProperty,
|
|
[SymbolToStringTag]: {
|
|
__proto__: null,
|
|
configurable: true,
|
|
value: 'Worker',
|
|
},
|
|
});
|
|
// };
|
|
|
|
// Worker includes AbstractWorker;
|
|
includeAbstractWorker(Worker.prototype);
|
|
// Worker includes MessageEventTarget;
|
|
includeMessageEventTarget(Worker.prototype);
|
|
|
|
// Operations of the worker global scope that are additionally exposed as
|
|
// own properties of the global object
|
|
const kScopeOperations = [
|
|
// EventTarget
|
|
'addEventListener', 'dispatchEvent', 'removeEventListener',
|
|
// WorkerGlobalScope
|
|
'importScripts',
|
|
// DedicatedWorkerGlobalScope
|
|
'postMessage', 'close',
|
|
];
|
|
|
|
/**
|
|
* @param {URL | string} url The worker's script URL.
|
|
* @param {{ name?: string, type?: string, port?: MessagePort }} [options]
|
|
* @returns {DedicatedWorkerGlobalScope} globalThis
|
|
*/
|
|
function installDedicatedWorkerGlobalScope(url, {
|
|
name = '',
|
|
type = 'classic',
|
|
port = require('worker_threads').parentPort,
|
|
} = kEmptyObject) {
|
|
const locationURL = new URL(url);
|
|
|
|
if (type === 'classic' && locationURL.protocol === 'file:') {
|
|
const { Module } = require('internal/modules/cjs/loader');
|
|
ObjectDefineProperty(globalThis, 'require', {
|
|
__proto__: null,
|
|
configurable: true,
|
|
writable: true,
|
|
value: Module.createRequire(locationURL),
|
|
});
|
|
}
|
|
|
|
const globalProto = ObjectGetPrototypeOf(globalThis);
|
|
ObjectSetPrototypeOf(globalProto, DedicatedWorkerGlobalScope.prototype);
|
|
ObjectDefineProperty(globalProto, 'constructor', {
|
|
__proto__: null,
|
|
configurable: true,
|
|
writable: true,
|
|
value: DedicatedWorkerGlobalScope,
|
|
});
|
|
|
|
initEventTarget(globalThis);
|
|
|
|
// "Create a new WorkerLocation object and associate it with worker global
|
|
// scope."
|
|
globalThis[kLocation] = new WorkerLocation(kCreate, locationURL);
|
|
globalThis[kName] = `${name}`;
|
|
globalThis[kType] = type;
|
|
// "DedicatedWorkerGlobalScope objects have an associated inside port (a
|
|
// MessagePort)."
|
|
// "Set worker global scope's inside port to inside port."
|
|
globalThis[kInsidePort] = port;
|
|
scopeBaseURL = locationURL;
|
|
|
|
if (port !== null) {
|
|
// "Set inside port's message event target to worker global scope."
|
|
forwardMessageEvents(port, globalThis);
|
|
}
|
|
|
|
// We install our own navigator, crypto and performance, see
|
|
// WorkerGlobalScope
|
|
ReflectDeleteProperty(globalThis, 'navigator');
|
|
ReflectDeleteProperty(globalThis, 'Navigator');
|
|
ReflectDeleteProperty(globalThis, 'crypto');
|
|
ReflectDeleteProperty(globalThis, 'performance');
|
|
|
|
for (let i = 0; i < kScopeOperations.length; i++) {
|
|
const key = kScopeOperations[i];
|
|
defineOperation(globalThis, key, assignFunctionName(
|
|
key, FunctionPrototypeBind(globalThis[key], globalThis)));
|
|
}
|
|
|
|
// [Exposed=Worker] and [Exposed=DedicatedWorker]
|
|
exposeInterface(globalThis, 'WorkerGlobalScope', WorkerGlobalScope);
|
|
exposeInterface(
|
|
globalThis, 'DedicatedWorkerGlobalScope', DedicatedWorkerGlobalScope);
|
|
exposeInterface(globalThis, 'WorkerNavigator', WorkerNavigator);
|
|
exposeInterface(globalThis, 'WorkerLocation', WorkerLocation);
|
|
return globalThis;
|
|
}
|
|
|
|
module.exports = {
|
|
Worker,
|
|
installDedicatedWorkerGlobalScope,
|
|
runClassicScriptSource,
|
|
runModuleScriptSource,
|
|
};
|