Skip to content
Merged
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
200 changes: 200 additions & 0 deletions test-app/app/src/main/assets/app/tests/testMessaging.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,206 @@ describe("Messaging runtime edges", function () {
}, SETTLE);
}, SETTLE);
});

var channelId = 0;
function channelName() {
return "messaging-edges-" + (channelId++);
}

function pollGC(predicate, cb) {
var turns = 0;
(function poll() {
__collect();
if (predicate() || turns >= 100) {
cb();
return;
}
turns++;
setTimeout(poll, 20);
})();
}

it("delivers a message posted before the listener was attached in the same turn", function (done) {
var name = channelName();
var sender = new BroadcastChannel(name);
var receiver = new BroadcastChannel(name);
sender.postMessage("early");
receiver.onmessage = function (event) {
expect(event.data).toBe("early");
sender.close();
receiver.close();
done();
};
});

it("discards a message that arrived while the channel had no listener", function (done) {
var name = channelName();
var sender = new BroadcastChannel(name);
var receiver = new BroadcastChannel(name);
var got = [];
sender.postMessage("unheard");
setTimeout(function () {
receiver.onmessage = function (event) { got.push(event.data); };
sender.postMessage("heard");
setTimeout(function () {
expect(got).toEqual(["heard"]);
sender.close();
receiver.close();
done();
}, SETTLE);
}, SETTLE);
});

it("stops delivering when the last listener goes and resumes when one returns", function (done) {
var name = channelName();
var sender = new BroadcastChannel(name);
var receiver = new BroadcastChannel(name);
var got = [];
var listener = function (event) { got.push(event.data); };
receiver.addEventListener("message", listener);
receiver.removeEventListener("message", listener);
sender.postMessage("gone");
setTimeout(function () {
receiver.addEventListener("message", listener);
sender.postMessage("back");
setTimeout(function () {
expect(got).toEqual(["back"]);
sender.close();
receiver.close();
done();
}, SETTLE);
}, SETTLE);
});

it("dispatches a single MessageEvent at the channel itself", function (done) {
var name = channelName();
var sender = new BroadcastChannel(name);
var receiver = new BroadcastChannel(name);
var events = [];
receiver.addEventListener("message", function (event) {
expect(this).toBe(receiver);
expect(event.currentTarget).toBe(receiver);
events.push(event);
});
receiver.onmessage = function (event) { events.push(event); };
sender.postMessage("once");
setTimeout(function () {
expect(events.length).toBe(2);
expect(events[0]).toBe(events[1]);
expect(events[0] instanceof MessageEvent).toBe(true);
expect(events[0].target).toBe(receiver);
sender.close();
receiver.close();
done();
}, SETTLE);
});

it("delivers to the channels of a group in creation order", function (done) {
var name = channelName();
var sender = new BroadcastChannel(name);
var receivers = [];
var order = [];
for (var i = 0; i < 16; i++) {
(function (index) {
var channel = new BroadcastChannel(name);
channel.onmessage = function () { order.push(index); };
receivers.push(channel);
})(i);
}
sender.postMessage("fan");
setTimeout(function () {
var expected = [];
for (var j = 0; j < receivers.length; j++) { expected.push(j); }
expect(order).toEqual(expected);
sender.close();
receivers.forEach(function (channel) { channel.close(); });
done();
}, SETTLE);
});

it("lets an unreferenced channel without a listener be collected", function (done) {
var wr = (function () {
return new WeakRef(new BroadcastChannel(channelName()));
})();
pollGC(function () { return wr.deref() === undefined; }, function () {
expect(wr.deref()).toBeUndefined();
done();
});
});

it("lets a channel be collected once its last listener is removed", function (done) {
var wr = (function () {
var channel = new BroadcastChannel(channelName());
channel.onmessage = function () {};
channel.onmessage = null;
return new WeakRef(channel);
})();
pollGC(function () { return wr.deref() === undefined; }, function () {
expect(wr.deref()).toBeUndefined();
done();
});
});

it("lets a closed channel be collected even with a listener", function (done) {
var wr = (function () {
var channel = new BroadcastChannel(channelName());
channel.onmessage = function () {};
channel.close();
return new WeakRef(channel);
})();
pollGC(function () { return wr.deref() === undefined; }, function () {
expect(wr.deref()).toBeUndefined();
done();
});
});

it("keeps an unreferenced channel with a messageerror listener alive", function (done) {
var wr = (function () {
var channel = new BroadcastChannel(channelName());
channel.onmessageerror = function () {};
return new WeakRef(channel);
})();
var turns = 0;
(function poll() {
__collect();
if (++turns < 10) {
setTimeout(poll, 20);
return;
}
var channel = wr.deref();
expect(channel instanceof BroadcastChannel).toBe(true);
if (channel) { channel.close(); }
done();
})();
});

it("keeps delivering to an unreferenced channel that has a listener", function (done) {
var name = channelName();
var sender = new BroadcastChannel(name);
var got = null;
var receiver = null;
(function () {
new BroadcastChannel(name).onmessage = function (event) {
got = event.data;
receiver = event.target;
};
})();
var turns = 0;
(function poll() {
__collect();
if (++turns < 10) {
setTimeout(poll, 20);
return;
}
sender.postMessage("still here");
setTimeout(function () {
expect(got).toBe("still here");
sender.close();
if (receiver) { receiver.close(); }
done();
}, SETTLE);
})();
});
});

describe("node:worker_threads", function () {
Expand Down
70 changes: 58 additions & 12 deletions test-app/runtime/src/main/cpp/Messaging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

#include <algorithm>
#include <initializer_list>
#include <set>
#include <shared_mutex>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

#include "ArgConverter.h"
#include "BuiltinLoader.h"
Expand Down Expand Up @@ -194,7 +194,10 @@ class SiblingGroup final : public std::enable_shared_from_this<SiblingGroup> {
// group is one channel's two ends.
const bool named_ = false;
std::shared_mutex mutex_;
std::set<PortData*> ports_;
// In join order: HTML delivers a broadcast to its destinations in the order
// the channels were created, and a burst that finds none of them already
// scheduled posts their drains in that order.
std::vector<PortData*> ports_;
};

namespace {
Expand Down Expand Up @@ -233,7 +236,7 @@ DispatchResult SiblingGroup::Dispatch(PortData* source, std::shared_ptr<Message>
std::string* error) {
std::shared_lock<std::shared_mutex> lock(this->mutex_);

if (this->ports_.find(source) == this->ports_.end()) {
if (std::find(this->ports_.begin(), this->ports_.end(), source) == this->ports_.end()) {
if (error != nullptr) {
*error = "Source MessagePort is not entangled with this group.";
}
Expand Down Expand Up @@ -278,7 +281,7 @@ void SiblingGroup::Entangle(PortData* port) {
void SiblingGroup::Entangle(std::initializer_list<PortData*> ports) {
std::unique_lock<std::shared_mutex> lock(this->mutex_);
for (PortData* data : ports) {
this->ports_.insert(data);
this->ports_.push_back(data);
// group_ is written under the port's own mutex, which is what lets
// PortData::Dispatch read it without racing a disentangle. Taken here in
// the only legal order: this group's lock is already held.
Expand All @@ -292,7 +295,8 @@ void SiblingGroup::Disentangle(PortData* data) {
// Keeps the group alive past the last member dropping its reference.
std::shared_ptr<SiblingGroup> self = this->shared_from_this();
std::unique_lock<std::shared_mutex> lock(this->mutex_);
this->ports_.erase(data);
this->ports_.erase(std::remove(this->ports_.begin(), this->ports_.end(), data),
this->ports_.end());
{
std::lock_guard<std::mutex> dataLock(data->mutex_);
data->group_.reset();
Expand All @@ -317,10 +321,11 @@ PortData::~PortData() {

void PortData::AddToIncomingQueue(std::shared_ptr<Message> message) {
std::lock_guard<std::mutex> lock(this->mutex_);
// Still holding the mutex: an owner read outside it could be detached by
// the time the wake reaches it, and Start() checks the backlog under it.
bool wake = this->owner_ != nullptr && this->owner_->WantsDrainFor(*message);
this->incoming_.push_back(std::move(message));
if (this->owner_ != nullptr) {
// Still holding the mutex: an owner read outside it could be detached by
// the time the wake reaches it.
if (wake) {
this->owner_->TriggerAsync();
}
}
Expand Down Expand Up @@ -397,6 +402,7 @@ std::shared_ptr<NativeMessagePort> NativeMessagePort::New(Local<Context> context
} else {
port->data_ = std::make_unique<PortData>(port.get());
if (group != nullptr) {
port->broadcast_ = true;
group->Entangle(port->data_.get());
}
}
Expand Down Expand Up @@ -449,6 +455,19 @@ void NativeMessagePort::Stop() {
this->receiving_ = false;
}

bool NativeMessagePort::WantsDrainFor(const Message& message) const {
// A port that was never started still learns that its sibling died.
return this->receiving_ || message.IsCloseMessage();
}

void NativeMessagePort::SetBroadcastTarget(Local<Object> target) {
if (target.IsEmpty()) {
this->broadcastTarget_.Reset();
} else {
this->broadcastTarget_.Reset(this->isolate_, target);
}
}

std::unique_ptr<PortData> NativeMessagePort::Detach() {
// owner_ drops under the data mutex, so a producer either wakes this port
// before the detach or never sees an owner at all. Node carries a separate
Expand All @@ -470,6 +489,7 @@ void NativeMessagePort::CloseHandle() {
0, nullptr, v8::kEmbedderDataTypeTagDefault);
this->wrapper_.Reset();
}
this->broadcastTarget_.Reset();
MessagingState* state = State(isolate);
if (state != nullptr) {
state->livePorts.erase(this->shared_from_this());
Expand Down Expand Up @@ -499,8 +519,9 @@ void NativeMessagePort::Close() {
}
// Last, on the wrapper the port has just let go of, so a listener finds an
// already-detached port and a close() from inside one is a no-op rather than
// a recursion.
if (!wrapper.IsEmpty()) {
// a recursion. A broadcast port's wrapper never reaches app code, so there
// is no one to tell.
if (!wrapper.IsEmpty() && !this->broadcast_) {
this->EmitClose(wrapper);
}
}
Expand Down Expand Up @@ -669,6 +690,17 @@ void NativeMessagePort::Drain() {
return;
}

Local<Object> receiver = wrapper;
if (this->broadcast_) {
// Per message: a listener can add or remove the channel's last listener.
// With none, the event would reach no one and reading the value has no
// other observable effect, so it is never read.
if (this->broadcastTarget_.IsEmpty()) {
continue;
}
receiver = this->broadcastTarget_.Get(isolate);
}

Local<Value> payload;
Local<Value> ports = v8::Undefined(isolate);
bool read;
Expand All @@ -693,12 +725,12 @@ void NativeMessagePort::Drain() {
}
}
if (!read) {
this->Emit(context, wrapper, emitMessage, payload, v8::Undefined(isolate),
this->Emit(context, receiver, emitMessage, payload, v8::Undefined(isolate),
"messageerror");
reschedule = true;
break;
}
if (!this->Emit(context, wrapper, emitMessage, payload, ports, "message")) {
if (!this->Emit(context, receiver, emitMessage, payload, ports, "message")) {
reschedule = true;
break;
}
Expand Down Expand Up @@ -882,6 +914,19 @@ void StopCallback(const FunctionCallbackInfo<Value>& info) {
}
}

void SetBroadcastTargetCallback(const FunctionCallbackInfo<Value>& info) {
Local<Object> wrapper;
if (!PortArg(info, 0, &wrapper)) {
return;
}
NativeMessagePort* port = PortFromWrapper(info.GetIsolate(), wrapper);
if (port == nullptr) {
return;
}
port->SetBroadcastTarget(info.Length() > 1 && info[1]->IsObject() ? info[1].As<Object>()
: Local<Object>());
}

void CloseCallback(const FunctionCallbackInfo<Value>& info) {
Local<Object> wrapper;
if (!PortArg(info, 0, &wrapper)) {
Expand Down Expand Up @@ -1038,6 +1083,7 @@ MaybeLocal<Object> CreateBinding(Local<Context> context) {
tns::SetMethod(context, binding, "postMessage", PostMessageCallback);
tns::SetMethod(context, binding, "start", StartCallback);
tns::SetMethod(context, binding, "stop", StopCallback);
tns::SetMethod(context, binding, "setBroadcastTarget", SetBroadcastTargetCallback);
tns::SetMethod(context, binding, "close", CloseCallback);
tns::SetMethod(context, binding, "drainOne", DrainOneCallback);
tns::SetMethodNoSideEffect(context, binding, "isDetached", IsDetachedCallback);
Expand Down
Loading
Loading