open static method

Future<Session> open({
  1. Config? config,
})

Opens a Zenoh session without blocking the calling isolate.

Canon's z_open blocks for a duration the configuration chooses, and it can be long. At the pinned zenoh-c 1.8.0, measured on Linux x64:

  • an unreachable configured endpoint — ~505 ms
  • an empty network at canon defaults — ~505 ms
  • a client-mode failure — ~3005 ms, which is scouting/timeout
  • multicast off with no endpoint — ~1 ms

⚠️ The ~500 ms against a reachable router is a defect in this pinned version, not canon's design: upstream PR #2493 fixes it in 1.9.0+, where the same cell reads 7–9 ms. What survives at every version is the unhappy set — empty network at defaults, dead endpoint, and client-mode failure at 3 s — which is exactly where a frozen UI matters most.

Four knobs move the wait, and all four are canon's, not this package's: scouting/delay · open/return_conditions/connect_scouted · open/return_conditions/declares · scouting/timeout (client mode's 3 s). ⚠️ Switching scouting/delay or connect_scouted is a 1.8.0 workaround, not standing advice. The wait is also a property of the pair: two peers that both have gossip enabled wait, and either one having it disabled removes it.

The blocking call therefore runs on a shim-owned thread and this returns a future. Timers keep firing, other futures keep progressing, and a Flutter frame pipeline keeps running for the whole wait.

If config is provided it is consumed and must not be reused or disposed by the caller. If config is null a default one is created internally.

An open in flight cannot be cancelled. Dropping the future does not stop the native call; the worker runs to completion either way.

Session carries no finalizer, so an un-awaited or un-closed session leaks by the same documented contract as every other release here: close is the only thing that reclaims it.

Throws StateError synchronously — before any future exists — if config has already been consumed or disposed. ⚠️ This deliberately diverges from Zenoh.scout, whose async body turns the same class of pre-flight error into a rejected future. Here the method is intentionally not async, so a programming error surfaces at the call rather than at the await.

Throws ZenohException two ways, and the channel discriminates: synchronously with a positive code if the background call could not be started (nothing ran, and no completion is coming), and as a rejected future carrying canon's negative code if z_open itself failed.

⚠️ A rejected future carries Z_ENETWORK (-4) for almost every cause — canon collapses them all into that one code, so the message tells you an open failed and not why. The reason lives in canon's own log. Turn it on with Zenoh.initLog('error'), or route it into your application with Zenoh.initLogWithSink; ⛔ the two are mutually exclusive and first-wins, so install the sink first if you may ever want one. See openFailureMessage for the full route, its leak condition, and why the stable build has only this one.

There is no openSync sibling.

Implementation

static Future<Session> open({Config? config}) {
  // Deliberately NOT an `async` body: everything down to the FFI call runs
  // synchronously, so a spent Config throws at the call site instead of
  // being wrapped into a rejected future.
  final effectiveConfig = config ?? Config();
  final configPtr = effectiveConfig.nativePtr;
  final callerSupplied = config != null;

  final receivePort = ReceivePort();
  final completer = Completer<Session>();

  receivePort.listen((dynamic message) {
    try {
      completeOpenFromPost(
        message,
        completer,
        callerSuppliedConfig: callerSupplied,
      );
    } finally {
      // Exactly one post is contracted, and ReceivePort.close is idempotent,
      // so closing here unconditionally both releases the port and stops the
      // isolate being pinned by a listener nobody will feed again.
      receivePort.close();
    }
  });

  final rc = bindings.zd_open_session_async(
    configPtr.cast(),
    receivePort.sendPort.nativePort,
  );

  // Unconditional, exactly as at zd_scout: the shim takes the config's
  // content via z_config_take before any fallible step, so it is consumed on
  // every path where the entry was reached with one. markConsumed detaches
  // the finalizer and frees the wrapper block without touching native.
  //
  // ⛔ NOT deferred to the post. Deferring would leave a dropped Config's
  // finalizer free to fire while the worker is still inside z_open.
  effectiveConfig.markConsumed();

  if (rc != 0) {
    // "Did it start" failed: nothing ran and no post will ever arrive, so
    // awaiting would hang forever. Close the port and throw SYNCHRONOUSLY --
    // a positive code, which is what tells the two failure classes apart.
    receivePort.close();
    throw ZenohException(openStartFailureMessage(rc), rc);
  }

  return completer.future;
}