Skip to content
LexerLangv0.1.0

Redis and sessions

The embedded RESP driver, the session module, cookie settings, flash data and conflict handling.

LexerLang
use redis

$redis = new redis({
    host: "localhost",
    port: 6379,
    db: 0,
    password: "1234"
}).connect()

The driver is built into the core: it speaks the RESP protocol directly, with no external Go package. There is no pooling — the same model as postgres.

CallReturns
$redis.connect()$redis (chainable)
$redis.ping()"PONG"
$redis.get($key)the value, or null
$redis.set($key, $value)$redis
$redis.set($key, $value, {lifetime: 60})$redis — lifetime in minutes
$redis.has($key)true / false
$redis.delete($k1, $k2, …)number deleted
$redis.expire($key, $minutes)true / false
$redis.ttl($key)remaining seconds, or null
$redis.increment($key)the new value
$redis.command("LPUSH", "queue", "job")the raw reply
$redis.close() / $redis.is_closed()

Objects and arrays are stored as JSON:

LexerLang
$redis.set("user", {id: 42, name: "Ali"})      # {"id":42,"name":"Ali"}

command is the escape hatch for commands the module does not expose.

Sessions#

LexerLang
use redis, session

$redis = new redis({host: "localhost", port: 6379, db: 0}).connect()

session.redis = $redis
session.start()

$user = session.get("user")
if $user == null then
    out("Not signed in")
end if

session.redis is the only assignable module field; modules are otherwise read-only.

Session state is specific to each request — the module is recreated for every use session and does not leak between requests.

Order matters: session.redis and session.config.set must come before start(), because start() reads the cookie with those settings.

The storage model#

LayerWhat it holds
Cookiethe session_id only
Redis keylexer:session:{id}
Redis value{id, version, created_at, last_activity, data, flash}
session.get/setthe data part only

An empty session is never written to Redis — no record is created until the first value is stored, so empty records do not pile up for visitors and bots that do nothing.

There is no secret setting: only a 128-bit random id goes into the cookie and the data stays in Redis. Signing is needed by designs that carry the data in the cookie.

Methods#

CallDescription
session.start()starts or resumes, returns the id
session.id()the current session_id
session.get($key[, $default])the value, else null / $default
session.set($key, $value)writes with a version check
session.has($key) · session.delete($key)
session.clear()clears the data, keeps the id
session.destroy()destroys the record, data and cookie
session.regenerate()new id, data carried over
session.all() · session.meta()
LexerLang
session.config.set({
    cookie_name: "lexer-id",        # default LEXER_SESSION_ID
    cookie_lifetime: 1440,          # minutes; default 120, 0 = session cookie
    cookie_path: "/",
    cookie_domain: "",              # empty = only the domain serving the request
    cookie_secure: true,            # default false
    cookie_httponly: true,          # default true
    cookie_samesite: "Lax"          # Lax | Strict | None
})

When SameSite: None is given, Secure is turned on automatically — browsers would otherwise ignore the cookie entirely.

Keys are validated. A misspelled setting is not silently ignored:

Text
session.config.set: bilinmeyen ayar "cookie_http_only";
                    "cookie_httponly" mi demek istediniz?

If it stayed quiet, a typo in cookie_secure would leave the cookie unprotected.

Flash data#

Data that lives until the next request.

LexerLang
session.flash("success", "Your profile was updated")   # writes
$message = session.flash("success")                    # reads and removes

If it is not read it survives one more request and is removed after that. Also: session.flash_has, session.flash_all, session.reflash, session.keep_flash.

Conflict handling#

The default is optimistic locking: each write checks the version, and on a conflict re-reads the record, replays the change onto the refreshed data and retries (three times by default). When those run out, SessionConflictError is raised.

To make several writes in one go:

LexerLang
session.with_lock(func()
    $n = session.get("counter", 0)
    session.set("counter", $n + 1)
end func)
LexerLang
try
    session.set("cart", $cart)
catch($e)
    if $e.type == "SessionConflictError" then
        out("session conflict")
    end if
end try

After signing in#

LexerLang
session.regenerate()
session.set("user_id", $user.id)

Regenerating the id after a successful sign-in defeats session fixation — an attacker planting a known id in the victim's browser beforehand.

Next#

Application state.