Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions NativeScript/runtime/NsBuiltinModules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "Runtime.h"
#include "StructuredSerialization.h"
#include "TextEncoding.h"
#include "Worker.h"

using namespace v8;

Expand All @@ -25,6 +26,7 @@ constexpr const char* kNodePrefix = "node:";
MaybeLocal<Object> NsModuleBinding(Local<Context> context);
MaybeLocal<Object> NsRuntimeBinding(Local<Context> context);
MaybeLocal<Object> NsUtilBinding(Local<Context> context);
MaybeLocal<Object> NsWorkerThreadsBinding(Local<Context> context);

struct Registration {
const char* specifier;
Expand All @@ -46,6 +48,7 @@ constexpr Registration kRegistry[] = {
{"ns:module", BuiltinId::kNsModule, NsModuleBinding},
{"ns:runtime", BuiltinId::kNsRuntime, NsRuntimeBinding},
{"ns:util", BuiltinId::kNsUtil, NsUtilBinding},
{"ns:worker_threads", BuiltinId::kNsWorkerThreads, NsWorkerThreadsBinding},
{"node:module", BuiltinId::kNodeModule, nullptr},
{"node:url", BuiltinId::kNodeUrl, nullptr},
{"node:util", BuiltinId::kNodeUtil, nullptr},
Expand Down Expand Up @@ -197,6 +200,21 @@ MaybeLocal<Object> NsRuntimeBinding(Local<Context> context) {
return binding;
}

// The Worker constructor for ns:worker_threads: the context's own, from the
// template Worker::Init installed, rather than whatever `globalThis.Worker`
// names by the time the module is first required.
MaybeLocal<Object> NsWorkerThreadsBinding(Local<Context> context) {
Isolate* isolate = v8::Isolate::GetCurrent();
Local<Object> binding = Object::New(isolate);
Local<v8::Function> worker;
if (!Worker::Constructor(context).ToLocal(&worker) ||
!binding->Set(context, tns::ToV8String(isolate, "Worker"), worker)
.FromMaybe(false)) {
return MaybeLocal<Object>();
}
return binding;
}

// TextEncoder / TextDecoder for ns:util, read straight out of the
// text-encoding builtin's per-isolate run, so the module's classes are the
// objects the globals of the same name expose.
Expand Down
7 changes: 7 additions & 0 deletions NativeScript/runtime/Worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ class Worker {
static void Init(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> globalTemplate);

// The Worker constructor of `context`: the function Init's template
// produces for it, which is the object the global of that name was created
// with, whatever `globalThis.Worker` names by now. Empty before Init has
// run for the isolate.
static v8::MaybeLocal<v8::Function> Constructor(
v8::Local<v8::Context> context);

// Turns Worker and the worker global scope into EventTargets and caches the
// builtin's delivery callout for this isolate. Runs during Runtime::Init,
// after Events::Init has installed the event primitives it builds on.
Expand Down
20 changes: 20 additions & 0 deletions NativeScript/runtime/Worker.mm
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
Global<v8::Function> emitEnded;
};

// The constructor template Init put on the global template. A template's
// function is cached per context, so instantiating it again yields the very
// function the global was created with.
struct WorkerConstructorState {
Global<FunctionTemplate> constructor;
};

} // namespace

std::vector<std::string> Worker::GlobalFunctions = {"postMessage", "close"};
Expand Down Expand Up @@ -275,6 +282,19 @@ bool ParseResourceLimits(Isolate* isolate, Local<Context> context, Local<Object>
prototype->Set(ToV8String(isolate, "terminate"), terminateWorkerFuncTemplate);

globalTemplate->Set(workerFuncName, workerFuncTemplate);

if (WorkerConstructorState* state = Caches::StateFor<WorkerConstructorState>(isolate)) {
state->constructor.Reset(isolate, workerFuncTemplate);
}
}

MaybeLocal<v8::Function> Worker::Constructor(Local<Context> context) {
Isolate* isolate = v8::Isolate::GetCurrent();
WorkerConstructorState* state = Caches::StateFor<WorkerConstructorState>(isolate);
if (state == nullptr || state->constructor.IsEmpty()) {
return MaybeLocal<v8::Function>();
}
return state->constructor.Get(isolate)->GetFunction(context);
}

void Worker::InitEvents(Local<Context> context) {
Expand Down
8 changes: 5 additions & 3 deletions NativeScript/runtime/js/node-worker-threads.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ function getCreateMessageEvent() {
}

const g = globalThis;
// The platform constructor this shim wraps, and the worker scope's channel
// back to its parent.
const NativeWorker = g.Worker;
// The platform constructor this shim wraps, taken from the standard module
// rather than from the global so an app that reassigns `globalThis.Worker`
// does not redirect the shim, and the worker scope's channel back to its
// parent.
const { Worker: NativeWorker } = require("ns:worker_threads");
const globalPostMessage = g.postMessage;

const addEventListener = EventTarget.prototype.addEventListener;
Expand Down
12 changes: 12 additions & 0 deletions NativeScript/runtime/js/ns-worker-threads.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use strict";

// The `ns:worker_threads` builtin module: the runtime's own Worker, the very
// function the global of that name was created with. See
// docs/ns-builtin-modules.md for the contract and docs/worker-threads.md for
// the constructor's options.

const { Worker } = binding;
const { ObjectFreeze } = primordials;

exports.Worker = Worker;
ObjectFreeze(exports);
66 changes: 66 additions & 0 deletions TestRunner/app/tests/NsWorkerThreadsTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
describe("ns:worker_threads", function () {
var nsWorkerThreads = require("ns:worker_threads");

it("exposes frozen exports", function () {
expect(Object.isFrozen(nsWorkerThreads)).toBe(true);
expect(typeof nsWorkerThreads.Worker).toBe("function");
});

// The export set is public API, declared in types/ns-worker-threads.d.ts
// and docs/ns-builtin-modules.md — all three must change together.
it("exposes exactly the declared surface", function () {
expect(Object.keys(nsWorkerThreads).sort()).toEqual(["Worker"]);
});

it("exports the Worker the global holds", function () {
expect(nsWorkerThreads.Worker).toBe(globalThis.Worker);
});

it("is a singleton per realm", function () {
expect(require("ns:worker_threads")).toBe(nsWorkerThreads);
});

it("is a distinct module object from the node:worker_threads shim", function () {
var shim = require("node:worker_threads");
expect(shim).not.toBe(nsWorkerThreads);
expect(Object.isFrozen(shim)).toBe(true);
});

it("constructs a worker with the runtime's options", function (done) {
var worker = new nsWorkerThreads.Worker("./workerResourceLimits/echoWorker.js", {
ios: { priority: "utility" },
resourceLimits: { maxOldGenerationSizeMb: 64 },
});
var settled = false;
var finish = function () {
if (settled) {
return;
}
settled = true;
worker.terminate();
done();
};
worker.onmessage = function (event) {
expect(event.data.started).toBe(true);
finish();
};
worker.onerror = function (event) {
expect(String(event.message)).toBe("<no worker error>");
finish();
};
});

it("hands a worker realm its own constructor, not whatever the global names", function (done) {
var worker = new Worker("./nsWorkerThreadsIdentityWorker.js");
worker.onmessage = function (event) {
expect(event.data).toEqual({ isOriginal: true, isImpostor: false, frozen: true });
worker.terminate();
done();
};
worker.onerror = function (event) {
worker.terminate();
fail("worker error: " + event.message);
done();
};
});
});
1 change: 1 addition & 0 deletions TestRunner/app/tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ require("./MetadataTests");
//
require("./ApiTests");
require("./NsRuntimeTests");
require("./NsWorkerThreadsTests");
require("./GCFinalizerTests");
require("./WorkerConcurrentStartupTests");
require("./WorkerOptionsTests");
Expand Down
10 changes: 10 additions & 0 deletions TestRunner/app/tests/nsWorkerThreadsIdentityWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Reassigns the global before the module is first required in this realm, so
// a module that merely read `globalThis.Worker` would report the impostor.
var original = globalThis.Worker;
globalThis.Worker = function Impostor() {};
var nsWorkerThreads = require("ns:worker_threads");
postMessage({
isOriginal: nsWorkerThreads.Worker === original,
isImpostor: nsWorkerThreads.Worker === globalThis.Worker,
frozen: Object.isFrozen(nsWorkerThreads),
});
34 changes: 31 additions & 3 deletions docs/ns-builtin-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,33 @@ workers for the change to reach them.** A worker started after the
`configureLoader` call resolves through the new vocabulary; a live worker
never observes a later reconfiguration.

### `ns:worker_threads`

The runtime's own `Worker`, reachable by specifier; the `node:worker_threads`
shim adapts it. **Experimental, iOS-only** until the Android runtime ships it.

| export | description |
|---|---|
| `Worker` | The runtime's `Worker` constructor: the very function the global of that name was created with, so `require("ns:worker_threads").Worker === globalThis.Worker` unless the app has reassigned the global. Its constructor options — `ios.priority` and Node's `resourceLimits` — are described in [worker-threads.md](worker-threads.md#worker-options). |

```js
import { Worker } from "ns:worker_threads";

const worker = new Worker("./heavy.js", {
ios: { priority: "utility" },
resourceLimits: { maxOldGenerationSizeMb: 64 },
});
```

The module exists so that typed code can name the runtime's constructor and
its options without depending on what `globalThis.Worker` resolves to in the
program's type environment. `types/ns-worker-threads.d.ts` declares the module and,
at script level, merges the two options into the global `WorkerOptions` and
declares the global `Worker` the way `@types/node` declares its own globals:
a program with a DOM lib keeps the lib's declaration, so
`new Worker(path, { ios })` type-checks either way without a conflicting
redeclaration.

### `node:` compatibility shims

The same registry serves the `node:` scheme with **compatibility shims** so
Expand Down Expand Up @@ -440,7 +467,7 @@ unmodified where a shim exists:
| `node:util` | `inspect`, `format`, `TextEncoder`, `TextDecoder` | Re-exports `ns:util`'s members unchanged (`nodeUtil.inspect === nsUtil.inspect`) from a **distinct, separately frozen module object**. `TextEncoder`/`TextDecoder` are the globals of those names, as they are in Node. Documented as partial. |
| `node:url` | `fileURLToPath`, `pathToFileURL` | Node-strict converters between `file:` URLs and paths. Documented as partial — no `URL`/`URLSearchParams` re-exports (both are globals), no legacy `url.parse`/`format`/`resolve`. |
| `node:module` | `createRequire` | Re-exports `ns:module`'s `createRequire` unchanged from a **distinct, separately frozen module object**. `createPumpingRequire` is deliberately absent: it has no Node counterpart, so code written against this shim keeps running on Node. `require.resolve`/`.cache`/`.main` are not implemented, and neither is any other `node:module` member (`Module`, `builtinModules`, `isBuiltin`, `register`, `syncBuiltinESMExports`). Documented as partial. |
| `node:worker_threads` | the messaging and thread surface — see [worker-threads.md](worker-threads.md) | The channel half (`MessagePort`, `MessageChannel`, `BroadcastChannel`, `receiveMessageOnPort`) is the real implementation, the same objects the globals of those names hold; the thread half is a bridge over the runtime's own `Worker`. It has no `ns:` counterpart — the surface tracks Node's, so there is nothing for a standard module to own. The one place it breaks the absent-not-throwing rule below is deliberate: `postMessageToThread` and `moveMessagePortToContext` are present and throw an `Error` naming themselves, because silently missing thread-addressed messaging reads as a delivery bug rather than as an unsupported call. Documented as partial. |
| `node:worker_threads` | the messaging and thread surface — see [worker-threads.md](worker-threads.md) | The channel half (`MessagePort`, `MessageChannel`, `BroadcastChannel`, `receiveMessageOnPort`) is the real implementation, the same objects the globals of those names hold; the thread half is a bridge over the `Worker` that `ns:worker_threads` exports. The channel half has no `ns:` counterpart — its surface tracks Node's, so there is nothing for a standard module to own. The one place it breaks the absent-not-throwing rule below is deliberate: `postMessageToThread` and `moveMessagePortToContext` are present and throw an `Error` naming themselves, because silently missing thread-addressed messaging reads as a delivery bug rather than as an unsupported call. Documented as partial. |

`node:url`'s parsing goes through the URL intrinsic, so `file://localhost/x` is
accepted (the URL spec folds a `localhost` authority to none) while any other
Expand Down Expand Up @@ -755,8 +782,9 @@ resolvers read the same table differently:
- The **`ns:`/`node:` resolver** — the app-facing one, behind `require()`,
`import` and `import()` — serves only rows *not* marked internal-only. An
internal-only specifier fails exactly as a name absent from the table does.
Seven rows are public today: `ns:module`, `ns:runtime`, `ns:util`,
`node:module`, `node:url`, `node:util`, `node:worker_threads`.
Eight rows are public today: `ns:module`, `ns:runtime`, `ns:util`,
`ns:worker_threads`, `node:module`, `node:url`, `node:util`,
`node:worker_threads`.
- The **internal require** builtins receive (previous section) is the only
thing that can name an internal-only row. Five rows are marked that way:
`internal/broadcast-channel`, `internal/dom-exception`, `internal/events`,
Expand Down
4 changes: 3 additions & 1 deletion docs/worker-threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,9 @@ transfer would strand the port's sibling.
The runtime's `Worker` constructor takes two options of its own, and the
`node:worker_threads` shim passes both through unchanged. Unknown keys inside
either object are ignored, so a later runtime can add more without breaking an
older one.
older one. The constructor is also exported by the
[`ns:worker_threads`](ns-builtin-modules.md#nsworker_threads) builtin, whose type declarations
describe both options and merge them into the global `WorkerOptions`.

### `ios.priority`

Expand Down
1 change: 1 addition & 0 deletions tools/js2c-inputs.xcfilelist
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ $(SRCROOT)/NativeScript/runtime/js/node-worker-threads.js
$(SRCROOT)/NativeScript/runtime/js/ns-module.js
$(SRCROOT)/NativeScript/runtime/js/ns-runtime.js
$(SRCROOT)/NativeScript/runtime/js/ns-util.js
$(SRCROOT)/NativeScript/runtime/js/ns-worker-threads.js
$(SRCROOT)/NativeScript/runtime/js/performance.js
$(SRCROOT)/NativeScript/runtime/js/promise-proxy.js
$(SRCROOT)/NativeScript/runtime/js/require-factory.js
Expand Down
1 change: 1 addition & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@
/// <reference path="./ns-module.d.ts" />
/// <reference path="./ns-runtime.d.ts" />
/// <reference path="./ns-util.d.ts" />
/// <reference path="./ns-worker-threads.d.ts" />
Loading
Loading