sourceafBedSheet::HttpSession.fan

using afIoc::Inject
using afIoc::Registry
using web::WebReq

** (Service) - An injectable 'const' version of [WebSession]`web::WebSession`.
** 
** Provides a name/value map associated with a specific browser *connection* to the web server.
** A cookie (with the name 'fanws') is used to track which session is made available to the request.
** 
** All values stored in the session must be serializable.
** 
** For scalable applications, the session should be used sparingly; house cleaned regularly and not used as a dumping ground. 
** 
** Flash 
** -----
** Whereas normal session values are persisted indefinitely, flash vales only exist until the end of the next request.
** After that, they are removed from the session.
** 
** A common usage is to store message values before a redirect (usually after a form post).
** When the following page is rendered, the message is retrieved and displayed.
** After which the message is automatically discarded from the session. 
** 
** *(Flash: A-ah - Saviour of the Universe!)*  
const mixin HttpSession {
    
    ** Get the unique id used to identify this session.
    ** 
    ** Calling this **will** create a session if it doesn't already exist.
    ** 
    ** @see `web::WebSession`
    abstract Str id()

    ** Returns 'true' if the session map is empty. 
    ** 
    ** Does not create a session if it does not already exist.
    virtual Bool isEmpty() {
        exists ? map.isEmpty : true
    }

    ** Returns 'true' if the session map contains the given key. 
    ** 
    ** Does not create a session if it does not already exist.
    virtual Bool containsKey(Str key) {
        exists ? map.containsKey(key) : false       
    }
    
    ** Convenience for 'map.get(name, def)'.
    ** 
    ** Does not create a session if it does not already exist.
    ** 
    ** @see `web::WebSession`
    @Operator
    Obj? get(Str name, Obj? def := null) {
        exists ? map.get(name, def) : def 
    }

    ** Convenience for 'map.getOrAdd(name, valFunc)'.
    ** 
    ** Calling this **will** create a session if it doesn't already exist.
    abstract Obj? getOrAdd(Str key, |Str->Obj?| valFunc)
    
    ** Convenience for 'map.set(name, val)'.
    ** 
    ** Calling this **will** create a session if it doesn't already exist.
    ** 
    ** @see `web::WebSession`
    @Operator 
    abstract Void set(Str name, Obj? val)
    
    ** Convenience for 'map.remove(name)'.
    ** 
    ** Does not create a session if it does not already exist.
    abstract Void remove(Str name)

    ** Application name/value pairs which are persisted between HTTP requests. 
    ** The values stored in this map must be serializable.
    ** 
    ** Calling this **will** create a session if it doesn't already exist.
    ** 
    ** The returned map is *READ ONLY*. 
    ** Use the methods on this class to write to the session.
    ** This is so we can *fail fast* (before a response is sent to the user) should a value not be serializable.
    ** 
    ** @see `web::WebSession`
    abstract Str:Obj? map()

    ** Delete this web session which clears both the user agent cookie and the server side session 
    ** instance. This method must be called before the WebRes is committed otherwise the server side 
    ** instance is cleared, but the user agent cookie will remain uncleared.
    ** 
    ** Does not create a session if it does not already exist.
    ** 
    ** @see `web::WebSession`
    abstract Void delete()
    
    ** Returns 'true' if a session exists. 
    ** 
    ** Does not create a session if it does not already exist.
    abstract Bool exists()
    
    ** Application name/value pairs which are persisted *only* until the user's next HTTP request. 
    ** Values stored in this map must be serializable.
    ** 
    ** Calling this **will** create a session if it doesn't already exist.
    ** 
    ** The returned map is *MODIFIABLE*. 
    ** 
    ** @see `web::WebSession`
    abstract Str:Obj? flash()
}

internal const class HttpSessionImpl : HttpSession {

    @Inject private const Registry      registry
    @Inject private const HttpCookies   httpCookies
    
    new make(|This|in) { in(this) } 

    override Str id() {
        webReq.session.id
    }

    override Str:Obj? map() {
        mapRw.ro
    }

    override Obj? getOrAdd(Str key, |Str->Obj?| valFunc) {
        if (!mapRw.containsKey(key)) {
            val := valFunc.call(key)
            mapRw[key] = testSerialisation(val)
        }
        return mapRw[key]
    }
    
    override Void set(Str name, Obj? val) { 
        mapRw[name] = testSerialisation(val)
    }
    
    override Void remove(Str name) {
        if (exists)
            mapRw.remove(name)      
    }
    
    override Void delete() {
        if (exists) {
            mapRw.clear
            webReq.session.delete
        }
    }

    override Bool exists() {
        // session support only for WISP web server
        httpCookies.get("fanws") != null
    }
    
    // TODO: replace flash map with a pseudo map so we can capture the get and set operations.
    // - benefits are, we don't create a session on read and split map up into a req and res 
    override Str:Obj? flash() {
        getOrAdd("afBedSheet.flash") { Str:Obj?[:] }
    }
    
    private Obj? testSerialisation(Obj? val) {
        Buf().out.writeObj(val, ["skipErrors":false])
        return val
    }
    
    private Str:Obj? mapRw() {
        webReq.session.map
    }
    
    private WebReq webReq() {
        registry.dependencyByType(WebReq#)
    }
}