Repository: apple/swift-nio
File: Sources/NIOCore/NIOLoopBound.swift (Swift)

Code before:
/// behaviour to do so.
///
/// If constructing a ``NIOLoopBoundBox`` with a `value`, it is also required the program to already be on `eventLoop`
/// but if you have a ``NIOLoopBoundBox`` that contains an ``Optional`` type, you may initialise it _without a value_
/// whilst off the ``EventLoop``. Any read/write access to `value` afterwards will require you to be on `eventLoop`.
public final class NIOLoopBoundBox<Value>: @unchecked Sendable {
    public let _eventLoop: EventLoop

    @usableFromInline
    /* private */var _value: Value

    /// Initialise a ``NIOLoopBoundBox`` to `value` with the precondition that the code is running on `eventLoop`.
    @inlinable
    public init(_ value: Value, eventLoop: EventLoop) {
        // This precondition is absolutely required. If not, it were possible to take a non-Sendable `Value` from
        // _off_ the ``EventLoop`` and transport it _to_ the ``EventLoop``. That would be illegal.
        eventLoop.preconditionInEventLoop()
        self._eventLoop = eventLoop
        self._value = value
    }

    /// Initialise a ``NIOLoopBoundBox`` with `nil`, this does _not_ require you to be running on `eventLoop`.
    public init<NonOptionalValue>(
        valueType: NonOptionalValue.Type = NonOptionalValue.self,
        eventLoop: EventLoop
    ) where Optional<NonOptionalValue> == Value {
        // Here, we -- possibly surprisingly -- do not precondition being on the EventLoop. This is okay for a few
        // reasons:
        // - We write the `Optional.none` value which we know is _not_ a value of the potentially non-Sendable type
        //   `Value`.
        // - Because of Swift's Definitive Initialisation (DI), we know that we did write `self._value` before `init`
        //   returns.
        // - The only way to ever write (or read indeed) `self._value` is by proving to be inside the `EventLoop`.
        self._value = .none
        self._eventLoop = eventLoop
    }

    /// Access the `value` with the precondition that the code is running on `eventLoop`.
    ///
    /// - note: ``NIOLoopBoundBox`` itself is reference-typed, so any writes will affect anybody sharing this reference.
    @inlinable
    public var value: Value {
        get {
            self._eventLoop.preconditionInEventLoop()
            return self._value
        }
        set {
            self._eventLoop.preconditionInEventLoop()
            self._value = newValue
        }
    }

Review comment (bug):
Usability nit: I'm not sure `NIOLoopBound(valueType: Foo.self, eventLoop: loop)` is not immediately obvious that it's for an optional value.

The other small usability concern is that you can also do `NIOLoopBoundBox<Foo?>(nil, eventLoop: loop)` off of the event loop and still crash when `NIOLoopBound(valueType: Foo.self, eventLoop: loop)` wouldn't.

I wonder if instead of this `init` we should overload the other init:

```swift
public init<NonOptionalType>(_ value: NonOptionalType?, eventLoop: EventLoop) where Optional<NonOptionalType> == Value {
    if value == nil { 
        // all good 
    } else { 
        // check on EL.
    }
}
```

We may have to mark the other `init` as `disfavoredOverload`

Code after:
/// behaviour to do so.
///
/// If constructing a ``NIOLoopBoundBox`` with a `value`, it is also required for the program to already be on `eventLoop`
/// but if you have a ``NIOLoopBoundBox`` that contains an ``Optional`` type, you may initialise it _without a value_
/// whilst off the ``EventLoop`` by using ``NIOLoopBoundBox.makeEmptyBox``. Any read/write access to `value`
/// afterwards will require you to be on `eventLoop`.
public final class NIOLoopBoundBox<Value>: @unchecked Sendable {
    public let _eventLoop: EventLoop

    @usableFromInline
    /* private */var _value: Value

    @inlinable
    internal init(_value value: Value, uncheckedEventLoop eventLoop: EventLoop) {
        self._eventLoop = eventLoop
        self._value = value
    }

    /// Initialise a ``NIOLoopBoundBox`` to `value` with the precondition that the code is running on `eventLoop`.
    @inlinable
    public convenience init(_ value: Value, eventLoop: EventLoop) {
        // This precondition is absolutely required. If not, it were possible to take a non-Sendable `Value` from
        // _off_ the ``EventLoop`` and transport it _to_ the ``EventLoop``. That would be illegal.
        eventLoop.preconditionInEventLoop()
        self.init(_value: value, uncheckedEventLoop: eventLoop)
    }

    /// Initialise a ``NIOLoopBoundBox`` that is empty (contains `nil`), this does _not_ require you to be running on `eventLoop`.
    public static func makeEmptyBox<NonOptionalValue>(
        valueType: NonOptionalValue.Type = NonOptionalValue.self,
        eventLoop: EventLoop
    ) -> NIOLoopBoundBox<Value> where Optional<NonOptionalValue> == Value {
        // Here, we -- possibly surprisingly -- do not precondition being on the EventLoop. This is okay for a few
        // reasons:
        // - We write the `Optional.none` value which we know is _not_ a value of the potentially non-Sendable type
        //   `Value`.
        // - Because of Swift's Definitive Initialisation (DI), we know that we did write `self._value` before `init`
        //   returns.
        // - The only way to ever write (or read indeed) `self._value` is by proving to be inside the `EventLoop`.
        return .init(_value: nil, uncheckedEventLoop: eventLoop)
    }

    /// Access the `value` with the precondition that the code is running on `eventLoop`.
    ///
    /// - note: ``NIOLoopBoundBox`` itself is reference-typed, so any writes will affect anybody sharing this reference.
    @inlinable
    public var value: Value {
        get {
            self._eventLoop.preconditionInEventLoop()
            return self._value
        }