1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "dispatch.h"
6
7 #include <cassert>
8 #include "cbor.h"
9 #include "error_support.h"
10 #include "find_by_first.h"
11 #include "frontend_channel.h"
12 #include "protocol_core.h"
13
14 namespace v8_crdtp {
15 // =============================================================================
16 // DispatchResponse - Error status and chaining / fall through
17 // =============================================================================
18
19 // static
Success()20 DispatchResponse DispatchResponse::Success() {
21 DispatchResponse result;
22 result.code_ = DispatchCode::SUCCESS;
23 return result;
24 }
25
26 // static
FallThrough()27 DispatchResponse DispatchResponse::FallThrough() {
28 DispatchResponse result;
29 result.code_ = DispatchCode::FALL_THROUGH;
30 return result;
31 }
32
33 // static
ParseError(std::string message)34 DispatchResponse DispatchResponse::ParseError(std::string message) {
35 DispatchResponse result;
36 result.code_ = DispatchCode::PARSE_ERROR;
37 result.message_ = std::move(message);
38 return result;
39 }
40
41 // static
InvalidRequest(std::string message)42 DispatchResponse DispatchResponse::InvalidRequest(std::string message) {
43 DispatchResponse result;
44 result.code_ = DispatchCode::INVALID_REQUEST;
45 result.message_ = std::move(message);
46 return result;
47 }
48
49 // static
MethodNotFound(std::string message)50 DispatchResponse DispatchResponse::MethodNotFound(std::string message) {
51 DispatchResponse result;
52 result.code_ = DispatchCode::METHOD_NOT_FOUND;
53 result.message_ = std::move(message);
54 return result;
55 }
56
57 // static
InvalidParams(std::string message)58 DispatchResponse DispatchResponse::InvalidParams(std::string message) {
59 DispatchResponse result;
60 result.code_ = DispatchCode::INVALID_PARAMS;
61 result.message_ = std::move(message);
62 return result;
63 }
64
65 // static
InternalError()66 DispatchResponse DispatchResponse::InternalError() {
67 DispatchResponse result;
68 result.code_ = DispatchCode::INTERNAL_ERROR;
69 result.message_ = "Internal error";
70 return result;
71 }
72
73 // static
ServerError(std::string message)74 DispatchResponse DispatchResponse::ServerError(std::string message) {
75 DispatchResponse result;
76 result.code_ = DispatchCode::SERVER_ERROR;
77 result.message_ = std::move(message);
78 return result;
79 }
80
81 // =============================================================================
82 // Dispatchable - a shallow parser for CBOR encoded DevTools messages
83 // =============================================================================
84 namespace {
85 constexpr size_t kEncodedEnvelopeHeaderSize = 1 + 1 + sizeof(uint32_t);
86 } // namespace
87
Dispatchable(span<uint8_t> serialized)88 Dispatchable::Dispatchable(span<uint8_t> serialized) : serialized_(serialized) {
89 Status s = cbor::CheckCBORMessage(serialized);
90 if (!s.ok()) {
91 status_ = {Error::MESSAGE_MUST_BE_AN_OBJECT, s.pos};
92 return;
93 }
94 cbor::CBORTokenizer tokenizer(serialized);
95 if (tokenizer.TokenTag() == cbor::CBORTokenTag::ERROR_VALUE) {
96 status_ = tokenizer.Status();
97 return;
98 }
99
100 // We checked for the envelope start byte above, so the tokenizer
101 // must agree here, since it's not an error.
102 assert(tokenizer.TokenTag() == cbor::CBORTokenTag::ENVELOPE);
103
104 // Before we enter the envelope, we save the position that we
105 // expect to see after we're done parsing the envelope contents.
106 // This way we can compare and produce an error if the contents
107 // didn't fit exactly into the envelope length.
108 const size_t pos_past_envelope = tokenizer.Status().pos +
109 kEncodedEnvelopeHeaderSize +
110 tokenizer.GetEnvelopeContents().size();
111 tokenizer.EnterEnvelope();
112 if (tokenizer.TokenTag() == cbor::CBORTokenTag::ERROR_VALUE) {
113 status_ = tokenizer.Status();
114 return;
115 }
116 if (tokenizer.TokenTag() != cbor::CBORTokenTag::MAP_START) {
117 status_ = {Error::MESSAGE_MUST_BE_AN_OBJECT, tokenizer.Status().pos};
118 return;
119 }
120 assert(tokenizer.TokenTag() == cbor::CBORTokenTag::MAP_START);
121 tokenizer.Next(); // Now we should be pointed at the map key.
122 while (tokenizer.TokenTag() != cbor::CBORTokenTag::STOP) {
123 switch (tokenizer.TokenTag()) {
124 case cbor::CBORTokenTag::DONE:
125 status_ =
126 Status{Error::CBOR_UNEXPECTED_EOF_IN_MAP, tokenizer.Status().pos};
127 return;
128 case cbor::CBORTokenTag::ERROR_VALUE:
129 status_ = tokenizer.Status();
130 return;
131 case cbor::CBORTokenTag::STRING8:
132 if (!MaybeParseProperty(&tokenizer))
133 return;
134 break;
135 default:
136 // We require the top-level keys to be UTF8 (US-ASCII in practice).
137 status_ = Status{Error::CBOR_INVALID_MAP_KEY, tokenizer.Status().pos};
138 return;
139 }
140 }
141 tokenizer.Next();
142 if (!has_call_id_) {
143 status_ = Status{Error::MESSAGE_MUST_HAVE_INTEGER_ID_PROPERTY,
144 tokenizer.Status().pos};
145 return;
146 }
147 if (method_.empty()) {
148 status_ = Status{Error::MESSAGE_MUST_HAVE_STRING_METHOD_PROPERTY,
149 tokenizer.Status().pos};
150 return;
151 }
152 // The contents of the envelope parsed OK, now check that we're at
153 // the expected position.
154 if (pos_past_envelope != tokenizer.Status().pos) {
155 status_ = Status{Error::CBOR_ENVELOPE_CONTENTS_LENGTH_MISMATCH,
156 tokenizer.Status().pos};
157 return;
158 }
159 if (tokenizer.TokenTag() != cbor::CBORTokenTag::DONE) {
160 status_ = Status{Error::CBOR_TRAILING_JUNK, tokenizer.Status().pos};
161 return;
162 }
163 }
164
ok() const165 bool Dispatchable::ok() const {
166 return status_.ok();
167 }
168
DispatchError() const169 DispatchResponse Dispatchable::DispatchError() const {
170 // TODO(johannes): Replace with DCHECK / similar?
171 if (status_.ok())
172 return DispatchResponse::Success();
173
174 if (status_.IsMessageError())
175 return DispatchResponse::InvalidRequest(status_.Message());
176 return DispatchResponse::ParseError(status_.ToASCIIString());
177 }
178
MaybeParseProperty(cbor::CBORTokenizer * tokenizer)179 bool Dispatchable::MaybeParseProperty(cbor::CBORTokenizer* tokenizer) {
180 span<uint8_t> property_name = tokenizer->GetString8();
181 if (SpanEquals(SpanFrom("id"), property_name))
182 return MaybeParseCallId(tokenizer);
183 if (SpanEquals(SpanFrom("method"), property_name))
184 return MaybeParseMethod(tokenizer);
185 if (SpanEquals(SpanFrom("params"), property_name))
186 return MaybeParseParams(tokenizer);
187 if (SpanEquals(SpanFrom("sessionId"), property_name))
188 return MaybeParseSessionId(tokenizer);
189 status_ =
190 Status{Error::MESSAGE_HAS_UNKNOWN_PROPERTY, tokenizer->Status().pos};
191 return false;
192 }
193
MaybeParseCallId(cbor::CBORTokenizer * tokenizer)194 bool Dispatchable::MaybeParseCallId(cbor::CBORTokenizer* tokenizer) {
195 if (has_call_id_) {
196 status_ = Status{Error::CBOR_DUPLICATE_MAP_KEY, tokenizer->Status().pos};
197 return false;
198 }
199 tokenizer->Next();
200 if (tokenizer->TokenTag() != cbor::CBORTokenTag::INT32) {
201 status_ = Status{Error::MESSAGE_MUST_HAVE_INTEGER_ID_PROPERTY,
202 tokenizer->Status().pos};
203 return false;
204 }
205 call_id_ = tokenizer->GetInt32();
206 has_call_id_ = true;
207 tokenizer->Next();
208 return true;
209 }
210
MaybeParseMethod(cbor::CBORTokenizer * tokenizer)211 bool Dispatchable::MaybeParseMethod(cbor::CBORTokenizer* tokenizer) {
212 if (!method_.empty()) {
213 status_ = Status{Error::CBOR_DUPLICATE_MAP_KEY, tokenizer->Status().pos};
214 return false;
215 }
216 tokenizer->Next();
217 if (tokenizer->TokenTag() != cbor::CBORTokenTag::STRING8) {
218 status_ = Status{Error::MESSAGE_MUST_HAVE_STRING_METHOD_PROPERTY,
219 tokenizer->Status().pos};
220 return false;
221 }
222 method_ = tokenizer->GetString8();
223 tokenizer->Next();
224 return true;
225 }
226
MaybeParseParams(cbor::CBORTokenizer * tokenizer)227 bool Dispatchable::MaybeParseParams(cbor::CBORTokenizer* tokenizer) {
228 if (params_seen_) {
229 status_ = Status{Error::CBOR_DUPLICATE_MAP_KEY, tokenizer->Status().pos};
230 return false;
231 }
232 params_seen_ = true;
233 tokenizer->Next();
234 if (tokenizer->TokenTag() == cbor::CBORTokenTag::NULL_VALUE) {
235 tokenizer->Next();
236 return true;
237 }
238 if (tokenizer->TokenTag() != cbor::CBORTokenTag::ENVELOPE) {
239 status_ = Status{Error::MESSAGE_MAY_HAVE_OBJECT_PARAMS_PROPERTY,
240 tokenizer->Status().pos};
241 return false;
242 }
243 params_ = tokenizer->GetEnvelope();
244 tokenizer->Next();
245 return true;
246 }
247
MaybeParseSessionId(cbor::CBORTokenizer * tokenizer)248 bool Dispatchable::MaybeParseSessionId(cbor::CBORTokenizer* tokenizer) {
249 if (!session_id_.empty()) {
250 status_ = Status{Error::CBOR_DUPLICATE_MAP_KEY, tokenizer->Status().pos};
251 return false;
252 }
253 tokenizer->Next();
254 if (tokenizer->TokenTag() != cbor::CBORTokenTag::STRING8) {
255 status_ = Status{Error::MESSAGE_MAY_HAVE_STRING_SESSION_ID_PROPERTY,
256 tokenizer->Status().pos};
257 return false;
258 }
259 session_id_ = tokenizer->GetString8();
260 tokenizer->Next();
261 return true;
262 }
263
264 namespace {
265 class ProtocolError : public Serializable {
266 public:
ProtocolError(DispatchResponse dispatch_response)267 explicit ProtocolError(DispatchResponse dispatch_response)
268 : dispatch_response_(std::move(dispatch_response)) {}
269
AppendSerialized(std::vector<uint8_t> * out) const270 void AppendSerialized(std::vector<uint8_t>* out) const override {
271 Status status;
272 std::unique_ptr<ParserHandler> encoder = cbor::NewCBOREncoder(out, &status);
273 encoder->HandleMapBegin();
274 if (has_call_id_) {
275 encoder->HandleString8(SpanFrom("id"));
276 encoder->HandleInt32(call_id_);
277 }
278 encoder->HandleString8(SpanFrom("error"));
279 encoder->HandleMapBegin();
280 encoder->HandleString8(SpanFrom("code"));
281 encoder->HandleInt32(static_cast<int32_t>(dispatch_response_.Code()));
282 encoder->HandleString8(SpanFrom("message"));
283 encoder->HandleString8(SpanFrom(dispatch_response_.Message()));
284 if (!data_.empty()) {
285 encoder->HandleString8(SpanFrom("data"));
286 encoder->HandleString8(SpanFrom(data_));
287 }
288 encoder->HandleMapEnd();
289 encoder->HandleMapEnd();
290 assert(status.ok());
291 }
292
SetCallId(int call_id)293 void SetCallId(int call_id) {
294 has_call_id_ = true;
295 call_id_ = call_id;
296 }
SetData(std::string data)297 void SetData(std::string data) { data_ = std::move(data); }
298
299 private:
300 const DispatchResponse dispatch_response_;
301 std::string data_;
302 int call_id_ = 0;
303 bool has_call_id_ = false;
304 };
305 } // namespace
306
307 // =============================================================================
308 // Helpers for creating protocol cresponses and notifications.
309 // =============================================================================
310
CreateErrorResponse(int call_id,DispatchResponse dispatch_response,const ErrorSupport * errors)311 std::unique_ptr<Serializable> CreateErrorResponse(
312 int call_id,
313 DispatchResponse dispatch_response,
314 const ErrorSupport* errors) {
315 auto protocol_error =
316 std::make_unique<ProtocolError>(std::move(dispatch_response));
317 protocol_error->SetCallId(call_id);
318 if (errors && !errors->Errors().empty()) {
319 protocol_error->SetData(
320 std::string(errors->Errors().begin(), errors->Errors().end()));
321 }
322 return protocol_error;
323 }
324
CreateErrorResponse(int call_id,DispatchResponse dispatch_response,const DeserializerState & state)325 std::unique_ptr<Serializable> CreateErrorResponse(
326 int call_id,
327 DispatchResponse dispatch_response,
328 const DeserializerState& state) {
329 auto protocol_error =
330 std::make_unique<ProtocolError>(std::move(dispatch_response));
331 protocol_error->SetCallId(call_id);
332 // TODO(caseq): should we plumb the call name here?
333 protocol_error->SetData(state.ErrorMessage(MakeSpan("params")));
334 return protocol_error;
335 }
336
CreateErrorNotification(DispatchResponse dispatch_response)337 std::unique_ptr<Serializable> CreateErrorNotification(
338 DispatchResponse dispatch_response) {
339 return std::make_unique<ProtocolError>(std::move(dispatch_response));
340 }
341
342 namespace {
343 class Response : public Serializable {
344 public:
Response(int call_id,std::unique_ptr<Serializable> params)345 Response(int call_id, std::unique_ptr<Serializable> params)
346 : call_id_(call_id), params_(std::move(params)) {}
347
AppendSerialized(std::vector<uint8_t> * out) const348 void AppendSerialized(std::vector<uint8_t>* out) const override {
349 Status status;
350 std::unique_ptr<ParserHandler> encoder = cbor::NewCBOREncoder(out, &status);
351 encoder->HandleMapBegin();
352 encoder->HandleString8(SpanFrom("id"));
353 encoder->HandleInt32(call_id_);
354 encoder->HandleString8(SpanFrom("result"));
355 if (params_) {
356 params_->AppendSerialized(out);
357 } else {
358 encoder->HandleMapBegin();
359 encoder->HandleMapEnd();
360 }
361 encoder->HandleMapEnd();
362 assert(status.ok());
363 }
364
365 private:
366 const int call_id_;
367 std::unique_ptr<Serializable> params_;
368 };
369
370 class Notification : public Serializable {
371 public:
Notification(const char * method,std::unique_ptr<Serializable> params)372 Notification(const char* method, std::unique_ptr<Serializable> params)
373 : method_(method), params_(std::move(params)) {}
374
AppendSerialized(std::vector<uint8_t> * out) const375 void AppendSerialized(std::vector<uint8_t>* out) const override {
376 Status status;
377 std::unique_ptr<ParserHandler> encoder = cbor::NewCBOREncoder(out, &status);
378 encoder->HandleMapBegin();
379 encoder->HandleString8(SpanFrom("method"));
380 encoder->HandleString8(SpanFrom(method_));
381 encoder->HandleString8(SpanFrom("params"));
382 if (params_) {
383 params_->AppendSerialized(out);
384 } else {
385 encoder->HandleMapBegin();
386 encoder->HandleMapEnd();
387 }
388 encoder->HandleMapEnd();
389 assert(status.ok());
390 }
391
392 private:
393 const char* method_;
394 std::unique_ptr<Serializable> params_;
395 };
396 } // namespace
397
CreateResponse(int call_id,std::unique_ptr<Serializable> params)398 std::unique_ptr<Serializable> CreateResponse(
399 int call_id,
400 std::unique_ptr<Serializable> params) {
401 return std::make_unique<Response>(call_id, std::move(params));
402 }
403
CreateNotification(const char * method,std::unique_ptr<Serializable> params)404 std::unique_ptr<Serializable> CreateNotification(
405 const char* method,
406 std::unique_ptr<Serializable> params) {
407 return std::make_unique<Notification>(method, std::move(params));
408 }
409
410 // =============================================================================
411 // DomainDispatcher - Dispatching betwen protocol methods within a domain.
412 // =============================================================================
WeakPtr(DomainDispatcher * dispatcher)413 DomainDispatcher::WeakPtr::WeakPtr(DomainDispatcher* dispatcher)
414 : dispatcher_(dispatcher) {}
415
~WeakPtr()416 DomainDispatcher::WeakPtr::~WeakPtr() {
417 if (dispatcher_)
418 dispatcher_->weak_ptrs_.erase(this);
419 }
420
421 DomainDispatcher::Callback::~Callback() = default;
422
dispose()423 void DomainDispatcher::Callback::dispose() {
424 backend_impl_ = nullptr;
425 }
426
Callback(std::unique_ptr<DomainDispatcher::WeakPtr> backend_impl,int call_id,span<uint8_t> method,span<uint8_t> message)427 DomainDispatcher::Callback::Callback(
428 std::unique_ptr<DomainDispatcher::WeakPtr> backend_impl,
429 int call_id,
430 span<uint8_t> method,
431 span<uint8_t> message)
432 : backend_impl_(std::move(backend_impl)),
433 call_id_(call_id),
434 method_(method),
435 message_(message.begin(), message.end()) {}
436
sendIfActive(std::unique_ptr<Serializable> partialMessage,const DispatchResponse & response)437 void DomainDispatcher::Callback::sendIfActive(
438 std::unique_ptr<Serializable> partialMessage,
439 const DispatchResponse& response) {
440 if (!backend_impl_ || !backend_impl_->get())
441 return;
442 backend_impl_->get()->sendResponse(call_id_, response,
443 std::move(partialMessage));
444 backend_impl_ = nullptr;
445 }
446
fallThroughIfActive()447 void DomainDispatcher::Callback::fallThroughIfActive() {
448 if (!backend_impl_ || !backend_impl_->get())
449 return;
450 backend_impl_->get()->channel()->FallThrough(call_id_, method_,
451 SpanFrom(message_));
452 backend_impl_ = nullptr;
453 }
454
DomainDispatcher(FrontendChannel * frontendChannel)455 DomainDispatcher::DomainDispatcher(FrontendChannel* frontendChannel)
456 : frontend_channel_(frontendChannel) {}
457
~DomainDispatcher()458 DomainDispatcher::~DomainDispatcher() {
459 clearFrontend();
460 }
461
sendResponse(int call_id,const DispatchResponse & response,std::unique_ptr<Serializable> result)462 void DomainDispatcher::sendResponse(int call_id,
463 const DispatchResponse& response,
464 std::unique_ptr<Serializable> result) {
465 if (!frontend_channel_)
466 return;
467 std::unique_ptr<Serializable> serializable;
468 if (response.IsError()) {
469 serializable = CreateErrorResponse(call_id, response);
470 } else {
471 serializable = CreateResponse(call_id, std::move(result));
472 }
473 frontend_channel_->SendProtocolResponse(call_id, std::move(serializable));
474 }
475
MaybeReportInvalidParams(const Dispatchable & dispatchable,const ErrorSupport & errors)476 bool DomainDispatcher::MaybeReportInvalidParams(
477 const Dispatchable& dispatchable,
478 const ErrorSupport& errors) {
479 if (errors.Errors().empty())
480 return false;
481 if (frontend_channel_) {
482 frontend_channel_->SendProtocolResponse(
483 dispatchable.CallId(),
484 CreateErrorResponse(
485 dispatchable.CallId(),
486 DispatchResponse::InvalidParams("Invalid parameters"), &errors));
487 }
488 return true;
489 }
490
MaybeReportInvalidParams(const Dispatchable & dispatchable,const DeserializerState & state)491 bool DomainDispatcher::MaybeReportInvalidParams(
492 const Dispatchable& dispatchable,
493 const DeserializerState& state) {
494 if (state.status().ok())
495 return false;
496 if (frontend_channel_) {
497 frontend_channel_->SendProtocolResponse(
498 dispatchable.CallId(),
499 CreateErrorResponse(
500 dispatchable.CallId(),
501 DispatchResponse::InvalidParams("Invalid parameters"), state));
502 }
503 return true;
504 }
505
clearFrontend()506 void DomainDispatcher::clearFrontend() {
507 frontend_channel_ = nullptr;
508 for (auto& weak : weak_ptrs_)
509 weak->dispose();
510 weak_ptrs_.clear();
511 }
512
weakPtr()513 std::unique_ptr<DomainDispatcher::WeakPtr> DomainDispatcher::weakPtr() {
514 auto weak = std::make_unique<DomainDispatcher::WeakPtr>(this);
515 weak_ptrs_.insert(weak.get());
516 return weak;
517 }
518
519 // =============================================================================
520 // UberDispatcher - dispatches between domains (backends).
521 // =============================================================================
DispatchResult(bool method_found,std::function<void ()> runnable)522 UberDispatcher::DispatchResult::DispatchResult(bool method_found,
523 std::function<void()> runnable)
524 : method_found_(method_found), runnable_(runnable) {}
525
Run()526 void UberDispatcher::DispatchResult::Run() {
527 if (!runnable_)
528 return;
529 runnable_();
530 runnable_ = nullptr;
531 }
532
UberDispatcher(FrontendChannel * frontend_channel)533 UberDispatcher::UberDispatcher(FrontendChannel* frontend_channel)
534 : frontend_channel_(frontend_channel) {
535 assert(frontend_channel);
536 }
537
538 UberDispatcher::~UberDispatcher() = default;
539
540 constexpr size_t kNotFound = std::numeric_limits<size_t>::max();
541
542 namespace {
DotIdx(span<uint8_t> method)543 size_t DotIdx(span<uint8_t> method) {
544 const void* p = memchr(method.data(), '.', method.size());
545 return p ? reinterpret_cast<const uint8_t*>(p) - method.data() : kNotFound;
546 }
547 } // namespace
548
Dispatch(const Dispatchable & dispatchable) const549 UberDispatcher::DispatchResult UberDispatcher::Dispatch(
550 const Dispatchable& dispatchable) const {
551 span<uint8_t> method = FindByFirst(redirects_, dispatchable.Method(),
552 /*default_value=*/dispatchable.Method());
553 size_t dot_idx = DotIdx(method);
554 if (dot_idx != kNotFound) {
555 span<uint8_t> domain = method.subspan(0, dot_idx);
556 span<uint8_t> command = method.subspan(dot_idx + 1);
557 DomainDispatcher* dispatcher = FindByFirst(dispatchers_, domain);
558 if (dispatcher) {
559 std::function<void(const Dispatchable&)> dispatched =
560 dispatcher->Dispatch(command);
561 if (dispatched) {
562 return DispatchResult(
563 true, [dispatchable, dispatched = std::move(dispatched)]() {
564 dispatched(dispatchable);
565 });
566 }
567 }
568 }
569 return DispatchResult(false, [this, dispatchable]() {
570 frontend_channel_->SendProtocolResponse(
571 dispatchable.CallId(),
572 CreateErrorResponse(dispatchable.CallId(),
573 DispatchResponse::MethodNotFound(
574 "'" +
575 std::string(dispatchable.Method().begin(),
576 dispatchable.Method().end()) +
577 "' wasn't found")));
578 });
579 }
580
581 template <typename T>
582 struct FirstLessThan {
operator ()v8_crdtp::FirstLessThan583 bool operator()(const std::pair<span<uint8_t>, T>& left,
584 const std::pair<span<uint8_t>, T>& right) {
585 return SpanLessThan(left.first, right.first);
586 }
587 };
588
WireBackend(span<uint8_t> domain,const std::vector<std::pair<span<uint8_t>,span<uint8_t>>> & sorted_redirects,std::unique_ptr<DomainDispatcher> dispatcher)589 void UberDispatcher::WireBackend(
590 span<uint8_t> domain,
591 const std::vector<std::pair<span<uint8_t>, span<uint8_t>>>&
592 sorted_redirects,
593 std::unique_ptr<DomainDispatcher> dispatcher) {
594 auto it = redirects_.insert(redirects_.end(), sorted_redirects.begin(),
595 sorted_redirects.end());
596 std::inplace_merge(redirects_.begin(), it, redirects_.end(),
597 FirstLessThan<span<uint8_t>>());
598 auto jt = dispatchers_.insert(dispatchers_.end(),
599 std::make_pair(domain, std::move(dispatcher)));
600 std::inplace_merge(dispatchers_.begin(), jt, dispatchers_.end(),
601 FirstLessThan<std::unique_ptr<DomainDispatcher>>());
602 }
603
604 } // namespace v8_crdtp
605