sourceafConcurrent::SynchronizedState.fan

using concurrent::Actor
using concurrent::ActorPool
using concurrent::AtomicInt
using concurrent::Future

** Provides 'synchronized' access to a (non- 'const') mutable state object.
**
** 'SynchronizedState' creates the state object in its own thread and provides access to it via the
** 'sync()' and 'async()' methods. Note that by their nature, these methods are immutable
** boundaries. Meaning that while data in the State object can be mutable, data passed in and out
** of these these boundaries can not be.
**
** 'SynchronizedState' is designed to be *scope safe*, that is you cannot accidently call methods
** on your State object outside of the 'sync()' and 'async()' methods.
**
** Example usage:
**
** pre>
** syntax: fantom
**
** sync := SynchronizedState(ActorPool(), Mutable#)
** msg  := "That's cool, dude!"
**
** val  := sync.sync |Mutable state -> Int| {
**     state.buf.writeChars(msg)
**     return state.buf.size
** }
**
** class Mutable {
**     Buf buf := Buf()
** }
** <pre
**
** Note 'SynchronizedState' overrides 'trap()' to make convenience calls to 'sync()' allowing dynamic calls:
**
** pre>
** syntax: fantom
** sync := SynchronizedState(ActorPool(), Mutable#)
**
** size := sync->buf->size  //--> returns size of Mutable.buf
** <pre
const class SynchronizedState {
    @NoDoc  // advanced use only
    const |->Obj?|      stateFactory
    @NoDoc  // advanced use only
    const LocalRef      stateRef

    ** The 'lock' object should you need to 'synchronize' on the state.
    const Synchronized  lock

    // Note we can't create a ctor(syncPool, type) 'cos that leads to ambiguous ctors in existing libs like afBedSheet / afParrotSdk2
    @NoDoc  // advanced use only - for setting own lock
    new make(|This| f) {
        f(this)
        if (this.stateRef == null)
            this.stateRef = LocalRef(SynchronizedState#.name)
    }

    ** Creates a 'SynchronizedState' instance.
    **
    ** The given state type must have a public no-args ctor as per [Type.make]`sys::Type.make`.
    new makeWithType(ActorPool actorPool, Type stateType) {
        this.lock           = Synchronized(actorPool)
        this.stateRef       = LocalRef(stateType.name)
        this.stateFactory   = |->Obj?| { stateType.make }
    }

    ** Creates a 'SynchronizedState' instance.
    **
    ** The given (immutable) factory func is used to create the state object inside it's thread.
    new makeWithFactory(ActorPool actorPool, |->Obj?| stateFactory) {
        this.lock           = Synchronized(actorPool)
        this.stateRef       = LocalRef(SynchronizedState#.name)
        this.stateFactory   = stateFactory
    }

    ** Legacy alias for calling 'sync()'.
    **
    ** Calls the given func synchronously, passing in the State object and returning the func's
    ** response.
    **
    ** The given func should be immutable.
    @NoDoc @Deprecated { msg="Use sync() instead" }
    Obj? getState(|Obj state -> Obj?| func) {
        sync(func)
    }

    ** Legacy alias for calling 'async()'.
    **
    ** Calls the given func asynchronously, passing in the State object.
    **
    ** The given func should be immutable.
    @NoDoc @Deprecated { msg="Use async() instead" }
    Future withState(|Obj state -> Obj?| func) {
        async(func)
    }

    ** Calls the given func synchronously, passing in the State object and returning the func's
    ** response.
    **
    ** The given func should be immutable.
    Obj? sync(|Obj state -> Obj?| func) {
        iFunc := func.toImmutable
        return lock.synchronized |->Obj?| { callFunc(iFunc) }
    }

    ** Calls the given func asynchronously, passing in the State object.
    **
    ** The given func should be immutable.
    Future async(|Obj state -> Obj?| func) {
        iFunc := func.toImmutable
        return lock.async |->Obj?| { callFunc(iFunc) }
    }

    ** Calls the given func asynchronously, passing in the State object.
    **
    ** The given func should be immutable.
    Future asyncLater(Duration duration, |Obj state -> Obj?| func) {
        iFunc := func.toImmutable
        return lock.asyncLater(duration) |->Obj?| { callFunc(iFunc) }
    }

    ** Routes trap() to the enclosed state object. Allows convenience calls for calling 'sync()'
    ** and returning state:
    **
    ** pre>
    ** syntax: fantom
    ** sync := SynchronizedState(ActorPool(), Buf#)
    **
    ** size := sync->size  //--> returns size of Buf
    ** <pre
    override Obj? trap(Str name, Obj?[]? args := null) {
        iargs := args?.toImmutable
        return sync |state->Obj?| {
            state.trap(name, iargs)
        }
    }

    private Obj? callFunc(|Obj?->Obj?| func) {
        if (stateRef.val == null)
            stateRef.val = stateFactory.call
        return func.call(stateRef.val)
    }

    ** Returns a string representation the state.
    override Str toStr() {
        sync { it.toStr }
    }
}