Skip to content

Commit

Permalink
feat: adding caching
Browse files Browse the repository at this point in the history
  • Loading branch information
Max Hill committed Sep 18, 2024
1 parent 7e352a1 commit 5c55581
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 15 deletions.
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ import wisp_kv_sessions/session_config
pub fn main() {
// Setup session_store
use actor_store <- result.map(actor_store.try_create_session_store())
use cache_store <- result.map(actor_store.try_create_session_store())
// Create session config
let session_config =
session_config.Config(
default_expiry: session.ExpireIn(60 * 60),
cookie_name: "SESSION_COOKIE",
store: actor_store,
cache: option.Some(cache_store),
)
let secret_key_base = wisp.random_string(64)
Expand Down Expand Up @@ -119,9 +121,21 @@ gleam test # Run the tests
just watch-test # Run test and reload on file changes
```

# SessionStore Drivers
# Caching

A SessionStore driver is a function that produces a SessionStore instance. These drivers allow for custom session storage solutions by applying methods to the SessionStore object, potentially using external resources like databases.
A session config can be passed an optional cache parameter that is a `SessionStore`
wrapped in an `option.Option`. If the cache is `option.Some` sessions will be
fetched from the cache. If the data is not in the cache the `store` will be
tried.

Session data will be automatically added and removed from the cache.

# SessionStore

A SessionStore is an type that can be used to implement different storage
providers, such as postgres/reddis/sqlite. You can use one of the prebuild
storage providers from down below, or implement a new one if the one you
are looking for does not exist.

For an example implementation, see `./src/wisp_kv_sessions/actor_store.gleam`.

Expand Down Expand Up @@ -163,3 +177,24 @@ use session_store <- result.map(postgres_store.try_create_session_store(conn))
//...
```
Further documentation can be found at <https://hexdocs.pm/wisp_kv_sessions_postgres_store>.


### ets_store

The ets_store uses [Erlang Term Storage](https://www.erlang.org/doc/apps/stdlib/ets.html)
and [carpenter](https://hexdocs.pm/carpenter/) to store session information.
*This will NOT be persistant after restarts*. But is a good option for caching.

```sh
gleam add wisp_kv_sessions_ets_store
```

```gleam
import wisp_kv_sessions/ets_store
// Setup session_store
use session_store <- result.map(postgres_store.try_create_session_store(conn))
//...
```
Further documentation can be found at <https://hexdocs.pm/wisp_kv_sessions_ets_store>.
2 changes: 2 additions & 0 deletions example/src/app.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import wisp_kv_sessions/session_config
pub fn main() {
// Setup session_store
use actor_store <- result.map(actor_store.try_create_session_store())
use cache_store <- result.map(actor_store.try_create_session_store())

// Create session config
let session_config =
session_config.Config(
default_expiry: session.ExpireIn(60 * 60),
cookie_name: "SESSION_COOKIE",
store: actor_store,
cache: option.Some(cache_store),
)

let secret_key_base = wisp.random_string(64)
Expand Down
58 changes: 47 additions & 11 deletions src/wisp_kv_sessions.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import wisp_kv_sessions/session_config
///
pub fn get_session(config: session_config.Config, req: wisp.Request) {
use session_id <- result.try(utils.get_session_id(config.cookie_name, req))
use maybe_session <- result.try(config.store.get_session(session_id))
use maybe_session <- result.try(get_session_with_cache(
session_id,
config,
True,
))

case maybe_session {
option.Some(session) -> {
case utils.is_session_expired(session) {
Expand All @@ -25,22 +30,52 @@ pub fn get_session(config: session_config.Config, req: wisp.Request) {
False -> Ok(session)
}
}
option.None ->
session.builder()
|> session.with_id(session_id)
|> session.with_expiry(config.default_expiry)
|> session.build
|> config.store.save_session
option.None -> {
let session =
session.builder()
|> session.with_id(session_id)
|> session.with_expiry(config.default_expiry)
|> session.build

save_session(config, session)
}
}
}

fn get_session_with_cache(
session_id: session.SessionId,
config: session_config.Config,
cache: Bool,
) {
case cache, config.cache {
True, option.Some(cache) -> {
case cache.get_session(session_id) {
Ok(option.Some(session)) -> Ok(option.Some(session))
_ -> get_session_with_cache(session_id, config, False)
}
}
_, _ -> {
config.store.get_session(session_id)
}
}
}

fn save_session(config: session_config.Config, session: session.Session) {
config.cache
|> option.map(fn(cache) { cache.save_session(session) })

config.store.save_session(session)
}

/// Remove session
/// Usage:
/// ```gleam
/// sessions.delete(store, req)
/// ```
pub fn delete_session(config: session_config.Config, req: wisp.Request) {
use session_id <- result.try(utils.get_session_id(config.cookie_name, req))
config.cache
|> option.map(fn(cache) { cache.delete_session(session_id) })
config.store.delete_session(session_id)
}

Expand Down Expand Up @@ -78,7 +113,7 @@ pub fn set(
session.builder_from(session)
|> session.set_key_value(key, json_data)
|> session.build
use _ <- result.map(config.store.save_session(new_session))
use _ <- result.map(save_session(config, new_session))
data
}

Expand All @@ -90,7 +125,8 @@ pub fn delete(
key: session.Key,
) {
use session <- result.try(get_session(config, req))
config.store.save_session(
save_session(
config,
session.Session(..session, data: dict.delete(session.data, key)),
)
}
Expand All @@ -108,7 +144,7 @@ pub fn replace_session(
new_session: session.Session,
) {
use _ <- result.try(delete_session(config, req))
use _ <- result.map(config.store.save_session(new_session))
use _ <- result.map(save_session(config, new_session))
utils.set_session_cookie(config.cookie_name, res, req, new_session)
}

Expand All @@ -134,7 +170,7 @@ pub fn middleware(
|> session.build()

// Try to save the session and fail silently
let _ = config.store.save_session
let _ = config.store.save_session(session)

let res =
utils.inject_session_cookie(
Expand Down
1 change: 0 additions & 1 deletion src/wisp_kv_sessions/actor_store.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ pub fn try_create_session_store() {
|> result.replace_error(session.DbSetupError),
)
session_config.SessionStore(
default_expiry: 60 * 60,
get_session: get_session(db),
save_session: save_session(db),
delete_session: delete_session(db),
Expand Down
2 changes: 1 addition & 1 deletion src/wisp_kv_sessions/session_config.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub type Config {
default_expiry: session.Expiry,
cookie_name: String,
store: SessionStore,
cache: option.Option(SessionStore),
)
}

Expand All @@ -16,7 +17,6 @@ pub type Config {
///
pub type SessionStore {
SessionStore(
default_expiry: Int,
get_session: fn(session.SessionId) ->
Result(option.Option(session.Session), session.SessionError),
save_session: fn(session.Session) ->
Expand Down
179 changes: 179 additions & 0 deletions test/cache_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import gleam/dict
import gleam/result
import gleeunit/should
import test_helpers
import wisp
import wisp/testing
import wisp_kv_sessions
import wisp_kv_sessions/session

pub fn get_value_from_cache_test() {
use #(session_config, main_store, cache_store, expires_at) <- result.map(
test_helpers.test_session_config_with_cache(),
)
let #(session, test_obj) = test_helpers.session_with_test_obj(expires_at)

use _ <- result.map(cache_store.save_session(session))

let req =
testing.get("/", [])
|> testing.set_cookie("SESSION_COOKIE", "TEST_SESSION_ID", wisp.Signed)

wisp_kv_sessions.get(
session_config,
req,
"test_key",
test_helpers.test_obj_from_json,
)
|> should.be_ok
|> should.be_some
|> should.equal(test_obj)

main_store.get_session(session.id)
|> should.be_ok
|> should.be_none
}

pub fn get_value_from_falls_back_to_main_test() {
use #(session_config, main_store, cache_store, expires_at) <- result.map(
test_helpers.test_session_config_with_cache(),
)
let #(session, test_obj) = test_helpers.session_with_test_obj(expires_at)
use _ <- result.map(main_store.save_session(session))

let req =
testing.get("/", [])
|> testing.set_cookie("SESSION_COOKIE", "TEST_SESSION_ID", wisp.Signed)

wisp_kv_sessions.get(
session_config,
req,
"test_key",
test_helpers.test_obj_from_json,
)
|> should.be_ok
|> should.be_some
|> should.equal(test_obj)

cache_store.get_session(session.id)
|> should.be_ok
|> should.be_none
}

pub fn set_value_in_session_test() {
use #(session_config, main_store, cache_store, expires_at) <- result.map(
test_helpers.test_session_config_with_cache(),
)
let #(session, test_obj) = test_helpers.session_with_test_obj(expires_at)

let req =
testing.get("/", [])
|> testing.set_cookie("SESSION_COOKIE", "TEST_SESSION_ID", wisp.Signed)

wisp_kv_sessions.set(
session_config,
req,
"test_key",
test_obj,
test_helpers.test_obj_to_json,
)
|> should.be_ok

cache_store.get_session(session.id)
|> should.be_ok
|> should.be_some
|> should.equal(session)

main_store.get_session(session.id)
|> should.be_ok
|> should.be_some
|> should.equal(session)
}

pub fn delete_value_from_cache_test() {
use #(session_config, main_store, cache_store, expires_at) <- result.try(
test_helpers.test_session_config_with_cache(),
)
let #(session, _test_obj) = test_helpers.session_with_test_obj(expires_at)

use _ <- result.map(cache_store.save_session(session))

let req =
testing.get("/", [])
|> testing.set_cookie("SESSION_COOKIE", "TEST_SESSION_ID", wisp.Signed)

wisp_kv_sessions.delete(session_config, req, "test_key")
|> should.be_ok

cache_store.get_session(session.id)
|> should.be_ok
|> should.be_some
|> fn(sess: session.Session) { dict.get(sess.data, "test_key") }
|> should.be_error

main_store.get_session(session.id)
|> should.be_ok
|> should.be_some
|> fn(sess: session.Session) { dict.get(sess.data, "test_key") }
|> should.be_error
}

pub fn get_session_test() {
use #(session_config, _main_store, cache_store, expires_at) <- result.try(
test_helpers.test_session_config_with_cache(),
)
let #(session, _test_obj) = test_helpers.session_with_test_obj(expires_at)

use _ <- result.map(cache_store.save_session(session))

let req =
testing.get("/", [])
|> testing.set_cookie("SESSION_COOKIE", "TEST_SESSION_ID", wisp.Signed)

wisp_kv_sessions.get_session(session_config, req)
|> should.be_ok()
|> should.equal(session)
}

pub fn get_session_falls_back_to_main_test() {
use #(session_config, main_store, _cache_store, expires_at) <- result.try(
test_helpers.test_session_config_with_cache(),
)
let #(session, _test_obj) = test_helpers.session_with_test_obj(expires_at)

use _ <- result.map(main_store.save_session(session))

let req =
testing.get("/", [])
|> testing.set_cookie("SESSION_COOKIE", "TEST_SESSION_ID", wisp.Signed)

wisp_kv_sessions.get_session(session_config, req)
|> should.be_ok()
|> should.equal(session)
}

pub fn caches_new_session_on_create_test() {
use #(session_config, _main_store, cache_store, expires_at) <- result.map(
test_helpers.test_session_config_with_cache(),
)

// This is the session that will be created
let session =
session.builder()
|> session.with_id_string("TEST_SESSION_ID")
|> session.with_expires_at(expires_at)
|> session.build

let req =
testing.get("/", [])
|> testing.set_cookie("SESSION_COOKIE", "TEST_SESSION_ID", wisp.Signed)

wisp_kv_sessions.get_session(session_config, req)
|> should.be_ok()
|> should.equal(session)

cache_store.get_session(session.id_from_string("TEST_SESSION_ID"))
|> should.be_ok()
|> should.be_some()
|> should.equal(session)
}
Loading

0 comments on commit 5c55581

Please sign in to comment.