[scenic] Add ObjectLinker::Link::ReleaseToken().

The new Flatland API functions for releasing a parent or child link also return
the link token to the caller so that a new link can be established. Right now,
the ObjectLinker consumes these tokens, but as soon as the link is resolved,
the tokens are discarded.

This change:
1. Makes the ObjectLinker hold onto the tokens in the resolved Endpoint rather
   than discarding them on link resolution.
2. Adds a function that allows the link holder to release the token.

When a link releases its token, it is invalidated, which results in the
link_invalidated callback being called if the link was initialized. If the link
was resolved, meaning it has an initialized peer, the peer will be
"unresolved". Unresolving a peer means:
1. The peer's link_invalidated callback will be called, but not erased.
2. The peer will be put back into the "initialized" state, meaning it retains
   its token and callbacks.

Even though the peer receives the link_invalidated callback, it does not need
to be initialized again. For example, if the released token is then used to
create a new link, the peer will receive the link_resolved callback again. This
means that any state set up in the link_resolved callback should be torn down
in the link_invalidated callback.

Fixed: 43745

TEST=fx shell runtests -t gfx_unittests

Change-Id: I3d27702e611402de22c2a99dc68f1fc07e51f374
diff --git a/src/ui/scenic/lib/gfx/engine/object_linker.cc b/src/ui/scenic/lib/gfx/engine/object_linker.cc
index 5c13ecc..81b162a 100644
--- a/src/ui/scenic/lib/gfx/engine/object_linker.cc
+++ b/src/ui/scenic/lib/gfx/engine/object_linker.cc
@@ -21,11 +21,26 @@
   }
 }
 
+void ObjectLinkerBase::Link::LinkUnresolved() {
+  if (link_invalidated_) {
+    link_invalidated_(false);
+  }
+}
+
+size_t ObjectLinkerBase::UnresolvedExportCount() {
+  return std::count_if(exports_.begin(), exports_.end(),
+                       [](const auto& iter) { return iter.second.IsUnresolved(); });
+}
+
+size_t ObjectLinkerBase::UnresolvedImportCount() {
+  return std::count_if(imports_.begin(), imports_.end(),
+                       [](const auto& iter) { return iter.second.IsUnresolved(); });
+}
+
 zx_koid_t ObjectLinkerBase::CreateEndpoint(zx::handle token, ErrorReporter* error_reporter,
                                            bool is_import) {
   // Select imports or exports to operate on based on the flag.
   auto& endpoints = is_import ? imports_ : exports_;
-  auto& unresolved_endpoints = is_import ? unresolved_imports_ : unresolved_exports_;
 
   if (!token) {
     error_reporter->ERROR() << "Token is invalid";
@@ -51,23 +66,18 @@
   // until Initialize() is called on the endpoint to provide a link object and
   // handler callbacks.
   Endpoint new_endpoint;
-  UnresolvedEndpoint new_unresolved_endpoint;
   new_endpoint.peer_endpoint_id = peer_endpoint_id;
-  new_unresolved_endpoint.peer_death_waiter = WaitForPeerDeath(token.get(), endpoint_id, is_import);
-  new_unresolved_endpoint.token = std::move(token);
-  auto emplaced_endpoint = endpoints.emplace(endpoint_id, new_endpoint);
-  auto emplaced_unresolved_endpoint =
-      unresolved_endpoints.emplace(endpoint_id, std::move(new_unresolved_endpoint));
+  new_endpoint.peer_death_waiter = WaitForPeerDeath(token.get(), endpoint_id, is_import);
+  new_endpoint.token = std::move(token);
+  auto emplaced_endpoint = endpoints.emplace(endpoint_id, std::move(new_endpoint));
   FXL_DCHECK(emplaced_endpoint.second);
-  FXL_DCHECK(emplaced_unresolved_endpoint.second);
 
   return endpoint_id;
 }
 
-void ObjectLinkerBase::DestroyEndpoint(zx_koid_t endpoint_id, bool is_import) {
+void ObjectLinkerBase::DestroyEndpoint(zx_koid_t endpoint_id, bool is_import, bool destroy_peer) {
   auto& endpoints = is_import ? imports_ : exports_;
   auto& peer_endpoints = is_import ? exports_ : imports_;
-  auto& unresolved_endpoints = is_import ? unresolved_imports_ : unresolved_exports_;
 
   auto endpoint_iter = endpoints.find(endpoint_id);
   if (endpoint_iter == endpoints.end()) {
@@ -78,26 +88,27 @@
 
   // If the object has a peer linked tell it about the object being removed,
   // which will immediately invalidate the peer.
-  zx_koid_t peer_endpoint_id = endpoint_iter->second.peer_endpoint_id;
-  auto peer_endpoint_iter = peer_endpoints.find(peer_endpoint_id);
-  if (peer_endpoint_iter != peer_endpoints.end()) {
-    Endpoint& peer_endpoint = peer_endpoint_iter->second;
+  if (destroy_peer) {
+    zx_koid_t peer_endpoint_id = endpoint_iter->second.peer_endpoint_id;
+    auto peer_endpoint_iter = peer_endpoints.find(peer_endpoint_id);
+    if (peer_endpoint_iter != peer_endpoints.end()) {
+      Endpoint& peer_endpoint = peer_endpoint_iter->second;
 
-    // Invalidate the peer endpoint.  If Initialize() has already been called on
-    // the peer endpoint, then close its connection which will destroy it.
-    // Otherwise, any future connection attempts will fail immediately with a
-    // link_failed callback, due to peer_endpoint_id being marked as
-    // invalid.
-    //
-    // This handles the case where the peer exists but Initialize() has not been
-    // called on it yet (so no callbacks exist).
-    peer_endpoint.peer_endpoint_id = ZX_KOID_INVALID;
-    FXL_DCHECK(peer_endpoint.link);
-    peer_endpoint.link->Invalidate(false);
+      // Invalidate the peer endpoint.  If Initialize() has already been called on
+      // the peer endpoint, then close its connection which will destroy it.
+      // Otherwise, any future connection attempts will fail immediately with a
+      // link_failed callback, due to peer_endpoint_id being marked as
+      // invalid.
+      //
+      // This handles the case where the peer exists but Initialize() has not been
+      // called on it yet (so no callbacks exist).
+      peer_endpoint.peer_endpoint_id = ZX_KOID_INVALID;
+      FXL_DCHECK(peer_endpoint.link);
+      peer_endpoint.link->Invalidate(/*on_destruction=*/false, /*invalidate_peer=*/true);
+    }
   }
 
   // At this point it is safe to completely erase the endpoint for the object.
-  unresolved_endpoints.erase(endpoint_id);
   endpoints.erase(endpoint_iter);
 }
 
@@ -120,7 +131,7 @@
   // endpoint is created, but before Initialize() is called on it.
   zx_koid_t peer_endpoint_id = endpoint.peer_endpoint_id;
   if (peer_endpoint_id == ZX_KOID_INVALID) {
-    link->Invalidate(false);
+    link->Invalidate(/*on_destruction=*/false, /*invalidate_peer=*/true);
     return;
   }
 
@@ -138,8 +149,6 @@
                                       bool is_import) {
   auto& endpoints = is_import ? imports_ : exports_;
   auto& peer_endpoints = is_import ? exports_ : imports_;
-  auto& unresolved_endpoints = is_import ? unresolved_imports_ : unresolved_exports_;
-  auto& peer_unresolved_endpoints = is_import ? unresolved_exports_ : unresolved_imports_;
 
   auto endpoint_iter = endpoints.find(endpoint_id);
   FXL_DCHECK(endpoint_iter != endpoints.end());
@@ -155,12 +164,9 @@
     return;  // Peer endpoint isn't connected yet, bail.
   }
 
-  // Destroy the pending entries (with the tokens and waiters) now that they
-  // are no longer useful.
-  size_t erase_count = unresolved_endpoints.erase(endpoint_id);
-  FXL_DCHECK(erase_count == 1);
-  size_t peer_erase_count = peer_unresolved_endpoints.erase(peer_endpoint_id);
-  FXL_DCHECK(peer_erase_count == 1);
+  // Delete the peer waiters now that the endpoints are resolved.
+  endpoint.peer_death_waiter = nullptr;
+  peer_endpoint.peer_death_waiter = nullptr;
 
   // Do linking last, so clients see a consistent view of the Linker.
   // Always fire the callback for the Export first, so clients can rely on
@@ -188,24 +194,24 @@
   static_assert(ZX_EVENTPAIR_PEER_CLOSED == __ZX_OBJECT_PEER_CLOSED, "enum mismatch");
   static_assert(ZX_FIFO_PEER_CLOSED == __ZX_OBJECT_PEER_CLOSED, "enum mismatch");
   static_assert(ZX_SOCKET_PEER_CLOSED == __ZX_OBJECT_PEER_CLOSED, "enum mismatch");
-  auto waiter = std::make_unique<async::Wait>(endpoint_handle, __ZX_OBJECT_PEER_CLOSED, 0,
-                                              std::bind([this, endpoint_id, is_import]() {
-                                                auto& endpoints = is_import ? imports_ : exports_;
-                                                auto endpoint_iter = endpoints.find(endpoint_id);
-                                                FXL_DCHECK(endpoint_iter != endpoints.end());
-                                                Endpoint& endpoint = endpoint_iter->second;
+  auto waiter = std::make_unique<async::Wait>(
+      endpoint_handle, __ZX_OBJECT_PEER_CLOSED, 0, std::bind([this, endpoint_id, is_import]() {
+        auto& endpoints = is_import ? imports_ : exports_;
+        auto endpoint_iter = endpoints.find(endpoint_id);
+        FXL_DCHECK(endpoint_iter != endpoints.end());
+        Endpoint& endpoint = endpoint_iter->second;
 
-                                                // Invalidate the endpoint.  If Initialize() has
-                                                // already been called on the endpoint, then close
-                                                // its connection (which will cause it to be
-                                                // destroyed).  Any future connection attempts will
-                                                // fail immediately with a link_failed call, due to
-                                                // peer_endpoint_id being marked as invalid.
-                                                endpoint.peer_endpoint_id = ZX_KOID_INVALID;
-                                                if (endpoint.link) {
-                                                  endpoint.link->Invalidate(false);
-                                                }
-                                              }));
+        // Invalidate the endpoint.  If Initialize() has
+        // already been called on the endpoint, then close
+        // its connection (which will cause it to be
+        // destroyed).  Any future connection attempts will
+        // fail immediately with a link_failed call, due to
+        // peer_endpoint_id being marked as invalid.
+        endpoint.peer_endpoint_id = ZX_KOID_INVALID;
+        if (endpoint.link) {
+          endpoint.link->Invalidate(/*on_destruction=*/false, /*invalidate_peer=*/true);
+        }
+      }));
 
   zx_status_t status = waiter->Begin(async_get_default_dispatcher());
   FXL_DCHECK(status == ZX_OK);
@@ -213,5 +219,33 @@
   return waiter;
 }
 
+zx::handle ObjectLinkerBase::ReleaseToken(zx_koid_t endpoint_id, bool is_import) {
+  auto& endpoints = is_import ? imports_ : exports_;
+  auto& peer_endpoints = is_import ? exports_ : imports_;
+
+  // If the endpoint was resolved, it will still be invalidated, but the peer endpoint must be
+  // unresolved first if it exists.
+  auto endpoint_iter = endpoints.find(endpoint_id);
+  FXL_DCHECK(endpoint_iter != endpoints.end());
+
+  zx_koid_t peer_endpoint_id = endpoint_iter->second.peer_endpoint_id;
+
+  auto peer_endpoint_iter = peer_endpoints.find(peer_endpoint_id);
+  if (peer_endpoint_iter == peer_endpoints.end()) {
+    return std::move(endpoint_iter->second.token);
+  }
+
+  // Signal that the link is now unresolved, then re-create the peer death waiter to flag the
+  // endpoint as unresolved.
+  if (peer_endpoint_iter->second.link) {
+    peer_endpoint_iter->second.link->LinkUnresolved();
+  }
+
+  peer_endpoint_iter->second.peer_death_waiter =
+      WaitForPeerDeath(peer_endpoint_iter->second.token.get(), peer_endpoint_id, !is_import);
+
+  return std::move(endpoint_iter->second.token);
+}
+
 }  // namespace gfx
 }  // namespace scenic_impl
diff --git a/src/ui/scenic/lib/gfx/engine/object_linker.h b/src/ui/scenic/lib/gfx/engine/object_linker.h
index 9e99472..d8fb98b7 100644
--- a/src/ui/scenic/lib/gfx/engine/object_linker.h
+++ b/src/ui/scenic/lib/gfx/engine/object_linker.h
@@ -30,9 +30,9 @@
   virtual ~ObjectLinkerBase() = default;
 
   size_t ExportCount() { return exports_.size(); }
-  size_t UnresolvedExportCount() { return unresolved_exports_.size(); }
+  size_t UnresolvedExportCount();
   size_t ImportCount() { return imports_.size(); }
-  size_t UnresolvedImportCount() { return unresolved_imports_.size(); }
+  size_t UnresolvedImportCount();
 
  protected:
   class Link {
@@ -40,10 +40,19 @@
     virtual ~Link() = default;
 
    protected:
-    virtual void Invalidate(bool on_destruction) = 0;
+    // When invalidating a link, the caller may choose to not invalidate the peer, which instead
+    // returns the peer to an initialized-but-unresolved state. This is primarily used when the
+    // caller releases the token of an existing link.
+    virtual void Invalidate(bool on_destruction, bool invalidate_peer) = 0;
     // Must be virtual so ObjectLinker::Link can pull the typed PeerObject from `peer_link`.
     virtual void LinkResolved(ObjectLinkerBase::Link* peer_link) = 0;
+
+    // Invalidating a link deletes the token it was created with, making the link permanently
+    // invalid and therefore allowing for the deletion of the |link_invalidated_| callback.
+    // Unresolving a link means its peer's token was released and may be used again, so the callback
+    // is called but not deleted.
     void LinkInvalidated(bool on_destruction);
+    void LinkUnresolved();
 
     fit::function<void(bool on_destruction)> link_invalidated_;
 
@@ -54,13 +63,10 @@
   struct Endpoint {
     zx_koid_t peer_endpoint_id = ZX_KOID_INVALID;
     Link* link = nullptr;
-  };
+    zx::handle token;                                // The token may be released by the link owner.
+    std::unique_ptr<async::Wait> peer_death_waiter;  // Only non-null if the link is unresolved.
 
-  // Information used to match one end of a link with its peer(s) on the
-  // other end.
-  struct UnresolvedEndpoint {
-    zx::handle token;  // Token for initial matching to peer endpoint.
-    std::unique_ptr<async::Wait> peer_death_waiter;
+    bool IsUnresolved() const { return peer_death_waiter != nullptr; }
   };
 
   // Only concrete ObjectLinker types should instantiate these.
@@ -76,7 +82,7 @@
   // Destroys the Endpoint pointed to by |endpoint_id| and removes all traces
   // of it from the linker.  If the Endpoint is linked to a peer, the peer
   // will be notified of the Endpoint's destruction.
-  void DestroyEndpoint(zx_koid_t endpoint_id, bool is_import);
+  void DestroyEndpoint(zx_koid_t endpoint_id, bool is_import, bool destroy_peer);
 
   // Puts the Endpoint pointed to by |endpoint_id| into an initialized state
   // by supplying it with an object and connection callbacks.  The Endpoint
@@ -95,10 +101,16 @@
   std::unique_ptr<async::Wait> WaitForPeerDeath(zx_handle_t endpoint_handle, zx_koid_t endpoint_id,
                                                 bool is_import);
 
+  // Releases the zx::handle for the Endpoint associated with |endpoint_id|, allowing the caller
+  // to establish a new link with it.
+  //
+  // This operation works regardless of whether or not the link has resolved. If the link was
+  // resolved, the peer Endpoint receives a |link_invalidated| callback and is put back in the
+  // initialized-but-unresolved state.
+  zx::handle ReleaseToken(zx_koid_t endpoint_id, bool is_import);
+
   std::unordered_map<zx_koid_t, Endpoint> exports_;
   std::unordered_map<zx_koid_t, Endpoint> imports_;
-  std::unordered_map<zx_koid_t, UnresolvedEndpoint> unresolved_exports_;
-  std::unordered_map<zx_koid_t, UnresolvedEndpoint> unresolved_imports_;
 
   FXL_DISALLOW_COPY_AND_ASSIGN(ObjectLinkerBase);
 };
@@ -149,9 +161,9 @@
     using PeerObj = typename std::conditional<is_import, Export, Import>::type;
 
     Link() = default;
-    virtual ~Link() { Invalidate(true); }
+    virtual ~Link() { Invalidate(/*on_destruction=*/true, /*invalidate_peer=*/true); }
     Link(Link&& other) { *this = std::move(other); }
-    Link& operator=(nullptr_t) { Invalidate(false); }
+    Link& operator=(nullptr_t) { Invalidate(/*on_destruction=*/false, /*invalidate_peer=*/true); }
     Link& operator=(Link&& other);
 
     bool valid() const { return linker_ && endpoint_id_ != ZX_KOID_INVALID; }
@@ -159,12 +171,19 @@
     zx_koid_t endpoint_id() const { return endpoint_id_; }
 
     // Initialize the Link with an |object| and callbacks for |link_resolved|
-    // and |link_failed| events, making it ready for connection to its peer.
-    // peer. The |link_failed| event is guaranteed to be called regardless of
+    // and |link_invalidated| events, making it ready for connection to its peer.
+    // peer. The |link_invalidated| event is guaranteed to be called regardless of
     // whether or not the |link_resolved| callback is, including if this Link
     // is destroyed, in which case |on_destruction| will be true.
     void Initialize(fit::function<void(PeerObj peer_object)> link_resolved = nullptr,
-                    fit::function<void(bool on_destruction)> link_failed = nullptr);
+                    fit::function<void(bool on_destruction)> link_invalidated = nullptr);
+
+    // Releases the zx::handle for this link, allowing the caller to establish a new link with it.
+    //
+    // This operation works regardless of whether or not the link has resolved. If the link was
+    // resolved, the peer receives a |link_invalidated| callback and is put back in the
+    // initialized-but-unresolved state.
+    std::optional<zx::handle> ReleaseToken();
 
    private:
     // Kept private so only an ObjectLinker can construct a valid Link.
@@ -172,7 +191,7 @@
         : object_(std::move(object)), endpoint_id_(endpoint_id), linker_(std::move(linker)) {}
 
     void LinkResolved(ObjectLinkerBase::Link* peer_link) override;
-    void Invalidate(bool on_destruction) override;
+    void Invalidate(bool on_destruction, bool invalidate_peer) override;
 
     std::optional<Obj> object_;
     zx_koid_t endpoint_id_ = ZX_KOID_INVALID;
@@ -233,7 +252,7 @@
 template <bool is_import>
 auto ObjectLinker<Export, Import>::Link<is_import>::operator=(Link&& other) -> Link& {
   // Invalidate the existing Link if its still valid.
-  Invalidate(false);
+  Invalidate(/*on_destruction=*/false, /*invalidate_peer=*/true);
 
   // Move data from the other Link and manually invalidate it, so it won't destroy
   // its endpoint when it dies.
@@ -268,9 +287,21 @@
 
 template <typename Export, typename Import>
 template <bool is_import>
-void ObjectLinker<Export, Import>::Link<is_import>::Invalidate(bool on_destruction) {
+std::optional<zx::handle> ObjectLinker<Export, Import>::Link<is_import>::ReleaseToken() {
+  if (!valid()) {
+    return std::optional<zx::handle>();
+  }
+  zx::handle token = linker_->ReleaseToken(endpoint_id_, is_import);
+  Invalidate(/*on_destruction=*/false, /*invalidate_peer=*/false);
+  return std::move(token);
+}
+
+template <typename Export, typename Import>
+template <bool is_import>
+void ObjectLinker<Export, Import>::Link<is_import>::Invalidate(bool on_destruction,
+                                                               bool invalidate_peer) {
   if (valid()) {
-    linker_->DestroyEndpoint(endpoint_id_, is_import);
+    linker_->DestroyEndpoint(endpoint_id_, is_import, invalidate_peer);
   }
   linker_.reset();
   object_.reset();
diff --git a/src/ui/scenic/lib/gfx/tests/object_linker_unittest.cc b/src/ui/scenic/lib/gfx/tests/object_linker_unittest.cc
index ae866c9..63197b2 100644
--- a/src/ui/scenic/lib/gfx/tests/object_linker_unittest.cc
+++ b/src/ui/scenic/lib/gfx/tests/object_linker_unittest.cc
@@ -9,6 +9,7 @@
 #include <zircon/types.h>
 
 #include "gtest/gtest.h"
+#include "src/lib/fsl/handles/object_info.h"
 #include "src/ui/scenic/lib/gfx/tests/error_reporting_test.h"
 
 namespace scenic_impl {
@@ -22,6 +23,16 @@
   std::bind([]() { EXPECT_TRUE(false) << "Delegate called unexpectedly: " << str; })
 
 class ObjectLinkerTest : public ErrorReportingTest {
+ public:
+  void TearDown() override {
+    ErrorReportingTest::TearDown();
+
+    EXPECT_EQ(0u, object_linker_.ExportCount());
+    EXPECT_EQ(0u, object_linker_.UnresolvedExportCount());
+    EXPECT_EQ(0u, object_linker_.ImportCount());
+    EXPECT_EQ(0u, object_linker_.UnresolvedImportCount());
+  }
+
  protected:
   struct TestExportObj {
     int value = 0;
@@ -757,7 +768,8 @@
 
 TEST_F(ObjectLinkerTest, ImportLinkDeathDestroysImport) {
   // Use a custom ObjectLinker template.
-  using SharedTestObjectLinker = ObjectLinker<std::shared_ptr<TestExportObj>, std::shared_ptr<TestImportObj>>;
+  using SharedTestObjectLinker =
+      ObjectLinker<std::shared_ptr<TestExportObj>, std::shared_ptr<TestImportObj>>;
   SharedTestObjectLinker object_linker;
 
   zx::eventpair export_token, import_token;
@@ -774,8 +786,7 @@
       object_linker.CreateImport(std::move(import_obj), std::move(import_token), error_reporter());
   EXPECT_SCENIC_SESSION_ERROR_COUNT(0);
 
-  import_link.Initialize(ERROR_IF_CALLED("import.link_resolved"),
-                         [](bool on_link_destruction) {});
+  import_link.Initialize(ERROR_IF_CALLED("import.link_resolved"), [](bool on_link_destruction) {});
 
   // This should cause the import to get a link_disconnected event when the eventloop ticks, which
   // will delete the only shared pointer to the object, invalidating the weak pointer.
@@ -787,7 +798,8 @@
 
 TEST_F(ObjectLinkerTest, ExportLinkDeathDestroysExport) {
   // Use a custom ObjectLinker template.
-  using SharedTestObjectLinker = ObjectLinker<std::shared_ptr<TestExportObj>, std::shared_ptr<TestImportObj>>;
+  using SharedTestObjectLinker =
+      ObjectLinker<std::shared_ptr<TestExportObj>, std::shared_ptr<TestImportObj>>;
   SharedTestObjectLinker object_linker;
 
   zx::eventpair export_token, import_token;
@@ -804,8 +816,7 @@
       object_linker.CreateExport(std::move(export_obj), std::move(export_token), error_reporter());
   EXPECT_SCENIC_SESSION_ERROR_COUNT(0);
 
-  export_link.Initialize(ERROR_IF_CALLED("import.link_resolved"),
-                         [](bool on_link_destruction) {});
+  export_link.Initialize(ERROR_IF_CALLED("import.link_resolved"), [](bool on_link_destruction) {});
 
   // This should cause the export to get a link_disconnected event when the eventloop ticks, which
   // will delete the only shared pointer to the object, invalidating the weak pointer.
@@ -815,6 +826,286 @@
   EXPECT_TRUE(weak_export_obj.expired());
 }
 
+TEST_F(ObjectLinkerTest, LinkOnlyReleasesTokenOnce) {
+  zx::eventpair export_token, import_token;
+  EXPECT_EQ(ZX_OK, zx::eventpair::create(0, &export_token, &import_token));
+
+  const zx_koid_t import_koid = fsl::GetKoid(import_token.get());
+  const zx_koid_t export_koid = fsl::GetKoid(export_token.get());
+
+  TestImportObj import_obj(kImportValue);
+  TestExportObj export_obj(kExportValue);
+
+  TestObjectLinker::ImportLink import_link =
+      object_linker_.CreateImport(std::move(import_obj), std::move(import_token), error_reporter());
+  TestObjectLinker::ExportLink export_link =
+      object_linker_.CreateExport(std::move(export_obj), std::move(export_token), error_reporter());
+
+  auto released_import = import_link.ReleaseToken();
+  EXPECT_TRUE(released_import.has_value());
+  EXPECT_EQ(fsl::GetKoid(released_import.value().get()), import_koid);
+
+  auto released_import2 = import_link.ReleaseToken();
+  EXPECT_FALSE(released_import2.has_value());
+
+  auto released_export = export_link.ReleaseToken();
+  EXPECT_TRUE(released_export.has_value());
+  EXPECT_EQ(fsl::GetKoid(released_export.value().get()), export_koid);
+
+  auto released_export2 = export_link.ReleaseToken();
+  EXPECT_FALSE(released_export2.has_value());
+}
+
+TEST_F(ObjectLinkerTest, ReleaseImportTokenBeforeInitialization) {
+  zx::eventpair export_token, import_token;
+  EXPECT_EQ(ZX_OK, zx::eventpair::create(0, &export_token, &import_token));
+
+  const zx_koid_t import_koid = fsl::GetKoid(import_token.get());
+
+  TestImportObj import_obj(kImportValue);
+
+  TestObjectLinker::ImportLink import_link =
+      object_linker_.CreateImport(std::move(import_obj), std::move(import_token), error_reporter());
+
+  // Releasing the token invalidates the |import_link|.
+  auto token = import_link.ReleaseToken();
+  EXPECT_TRUE(token.has_value());
+  EXPECT_EQ(fsl::GetKoid(token.value().get()), import_koid);
+
+  EXPECT_FALSE(import_link.valid());
+  EXPECT_EQ(object_linker_.ImportCount(), 0u);
+  EXPECT_EQ(object_linker_.UnresolvedImportCount(), 0u);
+}
+
+TEST_F(ObjectLinkerTest, ReleaseExportTokenBeforeInitialization) {
+  zx::eventpair export_token, import_token;
+  EXPECT_EQ(ZX_OK, zx::eventpair::create(0, &export_token, &import_token));
+
+  const zx_koid_t export_koid = fsl::GetKoid(export_token.get());
+
+  TestExportObj export_obj(kExportValue);
+
+  TestObjectLinker::ExportLink export_link =
+      object_linker_.CreateExport(std::move(export_obj), std::move(export_token), error_reporter());
+
+  // Releasing the token invalidates the |export_link|.
+  auto token = export_link.ReleaseToken();
+  EXPECT_TRUE(token.has_value());
+  EXPECT_EQ(fsl::GetKoid(token.value().get()), export_koid);
+
+  EXPECT_FALSE(export_link.valid());
+  EXPECT_EQ(object_linker_.ExportCount(), 0u);
+  EXPECT_EQ(object_linker_.UnresolvedExportCount(), 0u);
+}
+
+TEST_F(ObjectLinkerTest, ReleaseImportTokenAfterInitialization) {
+  zx::eventpair export_token, import_token;
+  EXPECT_EQ(ZX_OK, zx::eventpair::create(0, &export_token, &import_token));
+
+  const zx_koid_t import_koid = fsl::GetKoid(import_token.get());
+
+  TestImportObj import_obj(kImportValue);
+
+  bool import_disconnected = false;
+
+  TestObjectLinker::ImportLink import_link =
+      object_linker_.CreateImport(std::move(import_obj), std::move(import_token), error_reporter());
+  import_link.Initialize(ERROR_IF_CALLED("import.link_resolved"), [&](bool on_link_destruction) {
+    EXPECT_FALSE(on_link_destruction);
+    import_disconnected = true;
+  });
+
+  // Releasing the token from the |import_link| causes the invalidation of the link.
+  auto token = import_link.ReleaseToken();
+  EXPECT_TRUE(token.has_value());
+  EXPECT_EQ(fsl::GetKoid(token.value().get()), import_koid);
+
+  EXPECT_FALSE(import_link.valid());
+  EXPECT_TRUE(import_disconnected);
+  EXPECT_EQ(object_linker_.ImportCount(), 0u);
+  EXPECT_EQ(object_linker_.UnresolvedImportCount(), 0u);
+}
+
+TEST_F(ObjectLinkerTest, ReleaseExportTokenAfterInitialization) {
+  zx::eventpair export_token, import_token;
+  EXPECT_EQ(ZX_OK, zx::eventpair::create(0, &export_token, &import_token));
+
+  const zx_koid_t export_koid = fsl::GetKoid(export_token.get());
+
+  TestExportObj export_obj(kExportValue);
+
+  bool export_disconnected = false;
+
+  TestObjectLinker::ExportLink export_link =
+      object_linker_.CreateExport(std::move(export_obj), std::move(export_token), error_reporter());
+  export_link.Initialize(ERROR_IF_CALLED("export.link_resolved"), [&](bool on_link_destruction) {
+    EXPECT_FALSE(on_link_destruction);
+    export_disconnected = true;
+  });
+
+  // Releasing the token from the |export_link| causes the invalidation of the link.
+  auto token = export_link.ReleaseToken();
+  EXPECT_TRUE(token.has_value());
+  EXPECT_EQ(fsl::GetKoid(token.value().get()), export_koid);
+
+  EXPECT_FALSE(export_link.valid());
+  EXPECT_TRUE(export_disconnected);
+  EXPECT_EQ(object_linker_.ExportCount(), 0u);
+  EXPECT_EQ(object_linker_.UnresolvedExportCount(), 0u);
+}
+
+TEST_F(ObjectLinkerTest, ReleaseImportTokenAfterLinkResolution) {
+  zx::eventpair export_token, import_token;
+  EXPECT_EQ(ZX_OK, zx::eventpair::create(0, &export_token, &import_token));
+
+  TestImportObj import_obj(kImportValue);
+  TestExportObj export_obj(kExportValue);
+
+  int import_connected = 0;
+  int export_connected = 0;
+  int import_disconnected = 0;
+  int export_disconnected = 0;
+  int last_linked_import = 0;
+
+  TestObjectLinker::ImportLink import_link =
+      object_linker_.CreateImport(std::move(import_obj), std::move(import_token), error_reporter());
+  TestObjectLinker::ExportLink export_link =
+      object_linker_.CreateExport(std::move(export_obj), std::move(export_token), error_reporter());
+
+  import_link.Initialize(
+      [&import_connected](TestExportObj obj) { import_connected++; },
+      [&import_disconnected](bool on_link_destruction) { import_disconnected++; });
+  export_link.Initialize(
+      [&export_connected, &last_linked_import](TestImportObj obj) {
+        last_linked_import = obj.value;
+        export_connected++;
+      },
+      [&export_disconnected](bool on_link_destruction) { export_disconnected++; });
+
+  EXPECT_EQ(import_connected, 1);
+  EXPECT_EQ(export_connected, 1);
+  EXPECT_EQ(import_disconnected, 0);
+  EXPECT_EQ(export_disconnected, 0);
+  EXPECT_EQ(last_linked_import, kImportValue);
+
+  // Releasing the import token triggers the disconnect callbacks for both links. The export link
+  // remains valid but unresolved, but the import link becomes invalid.
+  auto token = import_link.ReleaseToken();
+  EXPECT_TRUE(token.has_value());
+  zx::eventpair import_token2 = zx::eventpair(std::move(token.value()));
+
+  RunLoopUntilIdle();
+
+  EXPECT_FALSE(import_link.valid());
+  EXPECT_TRUE(export_link.initialized());
+  EXPECT_EQ(import_disconnected, 1);
+  EXPECT_EQ(export_disconnected, 1);
+  EXPECT_EQ(object_linker_.UnresolvedImportCount(), 0u);
+  EXPECT_EQ(object_linker_.ImportCount(), 0u);
+  EXPECT_EQ(object_linker_.UnresolvedExportCount(), 1u);
+  EXPECT_EQ(object_linker_.ExportCount(), 1u);
+
+  // The import token can then be used to initialize a different ImportLink.
+  const int import_value2 = 2 * kImportValue;
+  TestImportObj import_obj2(import_value2);
+
+  int import_connected2 = 0;
+  int import_disconnected2 = 0;
+
+  TestObjectLinker::ImportLink import_link2 = object_linker_.CreateImport(
+      std::move(import_obj2), std::move(import_token2), error_reporter());
+
+  import_link2.Initialize(
+      [&import_connected2](TestExportObj obj) { import_connected2++; },
+      [&import_disconnected2](bool on_link_destruction) { import_disconnected2++; });
+
+  EXPECT_EQ(import_connected2, 1);
+  EXPECT_EQ(export_connected, 2);
+  EXPECT_EQ(import_disconnected2, 0);
+  EXPECT_EQ(export_disconnected, 1);
+  EXPECT_EQ(last_linked_import, import_value2);
+  EXPECT_EQ(object_linker_.UnresolvedImportCount(), 0u);
+  EXPECT_EQ(object_linker_.ImportCount(), 1u);
+  EXPECT_EQ(object_linker_.UnresolvedExportCount(), 0u);
+  EXPECT_EQ(object_linker_.ExportCount(), 1u);
+}
+
+TEST_F(ObjectLinkerTest, ReleaseExportTokenAfterLinkResolution) {
+  zx::eventpair export_token, import_token;
+  EXPECT_EQ(ZX_OK, zx::eventpair::create(0, &export_token, &import_token));
+
+  TestImportObj import_obj(kImportValue);
+  TestExportObj export_obj(kExportValue);
+
+  int import_connected = 0;
+  int export_connected = 0;
+  int import_disconnected = 0;
+  int export_disconnected = 0;
+  int last_linked_export = 0;
+
+  TestObjectLinker::ImportLink import_link =
+      object_linker_.CreateImport(std::move(import_obj), std::move(import_token), error_reporter());
+  TestObjectLinker::ExportLink export_link =
+      object_linker_.CreateExport(std::move(export_obj), std::move(export_token), error_reporter());
+
+  import_link.Initialize(
+      [&import_connected, &last_linked_export](TestExportObj obj) {
+        last_linked_export = obj.value;
+        import_connected++;
+      },
+      [&import_disconnected](bool on_link_destruction) { import_disconnected++; });
+  export_link.Initialize(
+      [&export_connected](TestImportObj obj) { export_connected++; },
+      [&export_disconnected](bool on_link_destruction) { export_disconnected++; });
+
+  EXPECT_EQ(import_connected, 1);
+  EXPECT_EQ(export_connected, 1);
+  EXPECT_EQ(import_disconnected, 0);
+  EXPECT_EQ(export_disconnected, 0);
+  EXPECT_EQ(last_linked_export, kExportValue);
+
+  // Releasing the export token triggers the disconnect callbacks for both links. The import link
+  // remains valid but unresolved, but the export link becomes invalid.
+  auto token = export_link.ReleaseToken();
+  EXPECT_TRUE(token.has_value());
+  zx::eventpair export_token2 = zx::eventpair(std::move(token.value()));
+
+  RunLoopUntilIdle();
+
+  EXPECT_FALSE(export_link.valid());
+  EXPECT_TRUE(import_link.initialized());
+  EXPECT_EQ(import_disconnected, 1);
+  EXPECT_EQ(export_disconnected, 1);
+  EXPECT_EQ(object_linker_.UnresolvedImportCount(), 1u);
+  EXPECT_EQ(object_linker_.ImportCount(), 1u);
+  EXPECT_EQ(object_linker_.UnresolvedExportCount(), 0u);
+  EXPECT_EQ(object_linker_.ExportCount(), 0u);
+
+  // The import token can then be used to initialize a different ExportLink.
+  const int export_value2 = 2 * kExportValue;
+  TestExportObj export_obj2(export_value2);
+
+  int export_connected2 = 0;
+  int export_disconnected2 = 0;
+
+  TestObjectLinker::ExportLink export_link2 = object_linker_.CreateExport(
+      std::move(export_obj2), std::move(export_token2), error_reporter());
+
+  export_link2.Initialize(
+      [&export_connected2](TestImportObj obj) { export_connected2++; },
+      [&export_disconnected2](bool on_link_destruction) { export_disconnected2++; });
+
+  EXPECT_EQ(import_connected, 2);
+  EXPECT_EQ(export_connected2, 1);
+  EXPECT_EQ(import_disconnected, 1);
+  EXPECT_EQ(export_disconnected2, 0);
+  EXPECT_EQ(last_linked_export, export_value2);
+  EXPECT_EQ(object_linker_.UnresolvedImportCount(), 0u);
+  EXPECT_EQ(object_linker_.ImportCount(), 1u);
+  EXPECT_EQ(object_linker_.UnresolvedExportCount(), 0u);
+  EXPECT_EQ(object_linker_.ExportCount(), 1u);
+}
+
 }  // namespace test
 }  // namespace gfx
 }  // namespace scenic_impl