sourceafConcurrent::AtomicMap.fan

using concurrent::AtomicRef

** A Map that provides fast reads and lightweight writes between threads using the copy on write paradigm. 
**
** The map is stored in an [AtomicRef]`concurrent::AtomicRef` through which all reads are made. 
** Writing makes a 'rw' copy of the map and is thus a more expensive operation.
** 
** > **CAUTION:** 
** Write operations ( 'getOrAdd', 'set', 'remove' & 'clear' ) are not synchronised. 
** This makes them lightweight but also susceptible to **data-loss** during race conditions.
** Though this may be acceptable for *caching* situations where values are re-calculated on demand.
** 
** All values held in the map must be immutable, except if you're caching funcs in a Javascipt environment. 
** 
** See the article [From One Thread to Another...]`http://www.fantomfactory.org/articles/from-one-thread-to-another#atomicMap` for more details.
@Js
const class AtomicMap {
    private const AtomicRef atomicMap := AtomicRef()

    ** The default value to use for `get` when a key isn't mapped.
    const Obj? def              := null
    
    ** Configures case sensitivity for maps with 'Str' keys.
    **
    **   syntax: fantom
    **  
    **   AtomicMap() { it.keyType = Str#; it.caseInsensitive = true }
    const Bool caseInsensitive  := false

    ** If 'true' the map will maintain the order in which key/value pairs are added.
    ** 
    **   syntax: fantom
    **  
    **   AtomicMap() { it.ordered = true }
    const Bool ordered          := false
    
    ** Used to parameterize the backing map.
    ** Must be non-nullable.
    ** 
    **   syntax: fantom
    **  
    **   AtomicMap() { it.keyType = Int# }
    const Type keyType          := Obj#
    
    ** Used to parameterize the backing map. 
    ** 
    **   syntax: fantom 
    ** 
    **   AtomicMap() { it.valType = Int# }
    const Type valType          := Obj?#
    
    @NoDoc  // pointless ctor!
    new make(|This|? f := null) { 
        f?.call(this) 
        if (caseInsensitive && keyType == Obj#)
            keyType = Str#
    }
    
    @NoDoc @Deprecated { msg="Use 'val' instead" }
    [Obj:Obj?] map {
        get { val }
        set { val = it }
    }

    ** Gets or sets a read-only copy of the backing map.
    [Obj:Obj?] val {
        get { 
            if (atomicMap.val == null)
                atomicMap.val = Map.make(Map#.parameterize(["K":keyType, "V":valType])) {
                    if (this.def != null)
                        it.def = this.def 
                    if (this.caseInsensitive) 
                        it.caseInsensitive = this.caseInsensitive 
                    it.ordered = this.ordered 
                }.toImmutable
            val := (Map) atomicMap.val
            if (Env.cur.runtime == "js")
                val = val.map { _unwrap(it) }
            return val
        }
        set {
            ConcurrentUtils.checkMapType(it.typeof, keyType, valType)
            val := it
            if (Env.cur.runtime == "js")
                val = val.map { _wrap(it) }
            atomicMap.val = val.toImmutable 
        }
    }
    
    ** Returns the value associated with the given key. If it doesn't exist then it is added from 
    ** the value function. 
    ** 
    ** This method is **NOT** thread safe. If two Actors call this method at the same time, the 
    ** value function will be called twice for the same key.
    Obj? getOrAdd(Obj key, |Obj key->Obj?| valFunc) {
        ConcurrentUtils.checkType(key.typeof, keyType, "Map key")
        iKey := key.toImmutable
        got  := get(iKey)       
        if (containsKey(iKey))
            return got
        val  := valFunc.call(iKey)
        ConcurrentUtils.checkType(val?.typeof, valType, "Map value")
        iVal := (Env.cur.runtime == "js") ? val : val?.toImmutable
        set(iKey, iVal)
        return iVal
    }

    ** Sets the key / value pair.
    @Operator
    Void set(Obj key, Obj? item) {
        ConcurrentUtils.checkType(key.typeof,  keyType, "Map key")
        ConcurrentUtils.checkType(item?.typeof, valType, "Map value")
        iKey  := key.toImmutable
        iVal  := (Env.cur.runtime == "js") ? item : item?.toImmutable
        rwMap := val.rw
        rwMap[iKey] = iVal
        val = rwMap
    }

    ** Remove all key/value pairs from the map. Return this.
    This clear() {
        val = val.rw.clear
        return this
    }

    ** Remove the key/value pair identified by the specified key
    ** from the map and return the value. 
    ** If the key was not mapped then return 'null'.
    Obj? remove(Obj key) {
        rwMap := val.rw
        oVal  := rwMap.remove(key)
        val = rwMap
        return oVal 
    }

    // ---- Common Map Methods --------------------------------------------------------------------

    ** Returns 'true' if the map contains the given key
    Bool containsKey(Obj key) {
        val.containsKey(key)
    }
    
    ** Call the specified function for every key/value in the map.
    Void each(|Obj? item, Obj key| c) {
        val.each(c)
    }

    ** Returns the value associated with the given key. 
    ** If key is not mapped, then return the value of the 'def' parameter.  
    ** If 'def' is omitted it defaults to 'null'.
    @Operator
    Obj? get(Obj key, Obj? def := this.def) {
        val.get(key, def)
    }
    
    ** Return 'true' if size() == 0
    Bool isEmpty() {
        val.isEmpty
    }

    ** Returns a list of all the mapped keys.
    Obj[] keys() {
        val.keys
    }

    ** Get a read-write, mutable Map instance with the same contents.
    [Obj:Obj?] rw() {
        val.rw
    }

    ** Get the number of key/value pairs in the map.
    Int size() {
        val.size
    }

    ** Returns a list of all the mapped values.
    Obj?[] vals() {
        val.vals
    }
    
    ** Returns a string representation the map.
    override Str toStr() {
        val.toStr
    }
    
    private Obj? _unwrap(Obj? obj) {
        (obj is Unsafe && ((Unsafe) obj).val is Func) ? ((Unsafe) obj).val : obj
    }

    private Obj? _wrap(Obj? obj) {
        (obj is Func) ? Unsafe(obj) : obj
    }
}