Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 38 additions & 26 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,40 +1,52 @@
[package]
name = "async-session"
version = "3.0.0"
name = "saysion"
version = "0.1.2"
license = "MIT OR Apache-2.0"
repository = "https://github.com/http-rs/async-session"
documentation = "https://docs.rs/async-session"
repository = "https://github.com/salvo-rs/saysion"
documentation = "https://docs.rs/saysion"
description = "Async session support with pluggable stores"
readme = "README.md"
edition = "2021"
edition = "2024"
rust-version = "1.89"
keywords = []
categories = []
authors = [
"Yoshua Wuyts <yoshuawuyts@gmail.com>",
"Jacob Rothstein <hi@jbr.me>"
"Jacob Rothstein <hi@jbr.me>",
"chrislearn <chris@acroidea.com>"
]

[dependencies]
async-trait = "0.1.59"
rand = "0.8.5"
base64 = "0.20.0"
sha2 = "0.10.6"
hmac = "0.12.1"
serde_json = "1.0.89"
bincode = "1.3.3"
anyhow = "1.0.66"
blake3 = "1.3.3"
async-lock = "2.6.0"
log = "0.4.17"
async-trait = "0.1"
rand = "0.9"
base64 = "0.22"
sha2 = "0.10"
hmac = "0.12"
serde = { version = "1", features = ["derive", "rc"] }
serde_json = "1"
postcard = { version = "1", default-features = false, features = ["use-std"] }
anyhow = "1"
blake3 = "1"
async-lock = "3"
time = { version = "0.3", features = ["serde"] }
tracing = "0.1"

[dependencies.serde]
version = "1.0.150"
features = ["rc", "derive"]
# Optional backends
redis = { version = "0.27", default-features = false, features = ["tokio-comp", "connection-manager"], optional = true }
mongodb = { version = "3", optional = true }
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls"], optional = true }

[dependencies.time]
version = "0.3.17"
features = ["serde"]
[features]
default = []
redis = ["dep:redis"]
mongodb = ["dep:mongodb"]
sqlx = ["dep:sqlx"]
sqlx-postgres = ["sqlx", "sqlx/postgres"]
sqlx-sqlite = ["sqlx", "sqlx/sqlite"]
sqlx-mysql = ["sqlx", "sqlx/mysql"]

[dev-dependencies.async-std]
version = "1.12.0"
features = ["attributes"]
[package.metadata.docs.rs]
all-features = true

[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
49 changes: 6 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,30 @@
<h1 align="center">async-session</h1>
<h1 align="center">saysion</h1>
<div align="center">
<strong>
Async session support with pluggable middleware
Session support with pluggable middleware
</strong>
</div>

<br />

<div align="center">
<!-- Crates version -->
<a href="https://crates.io/crates/async-session">
<img src="https://img.shields.io/crates/v/async-session.svg?style=flat-square"
alt="Crates.io version" />
</a>
<!-- Downloads -->
<a href="https://crates.io/crates/async-session">
<img src="https://img.shields.io/crates/d/async-session.svg?style=flat-square"
alt="Download" />
</a>
<!-- docs.rs docs -->
<a href="https://docs.rs/async-session">
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
alt="docs.rs docs" />
</a>
</div>

<div align="center">
<h3>
<a href="https://docs.rs/async-session">
<a href="https://docs.rs/saysion">
API Docs
</a>
<span> | </span>
<a href="https://github.com/http-rs/async-session/releases">
<a href="https://github.com/salvo-rs/saysion/releases">
Releases
</a>
<span> | </span>
<a href="https://github.com/http-rs/async-session/blob/main/.github/CONTRIBUTING.md">
<a href="https://github.com/salvo-rs/saysion/blob/main/.github/CONTRIBUTING.md">
Contributing
</a>
</h3>
</div>

## Available session stores

* [async-sqlx-session](https://crates.io/crates/async-sqlx-session) postgres, mysql & sqlite
* [async-redis-session](https://crates.io/crates/async-redis-session)
* [async-mongodb-session](https://crates.io/crates/async-mongodb-session)
* [async-session-r2d2](https://crates.io/crates/async-session-r2d2) - sqlite only

## Framework implementations

* [`tide::sessions`](https://docs.rs/tide/latest/tide/sessions/index.html)
* [warp-sessions](https://docs.rs/warp-sessions/latest/warp_sessions/)
* [trillium-sessions](https://docs.trillium.rs/trillium_sessions)
* [axum-sessions](https://docs.rs/axum_sessions)
* [salvo-sessions](https://docs.rs/salvo_extra/latest/salvo_extra/session/index.html)
*Fork from: https://github.com/http-rs/async-session*

## Safety
This crate uses ``#![deny(unsafe_code)]`` to ensure everything is implemented in
Expand All @@ -64,13 +34,6 @@ This crate uses ``#![deny(unsafe_code)]`` to ensure everything is implemented in
Want to join us? Check out our ["Contributing" guide][contributing] and take a
look at some of these issues:

- [Issues labeled "good first issue"][good-first-issue]
- [Issues labeled "help wanted"][help-wanted]

[contributing]: https://github.com/http-rs/async-session/blob/main/.github/CONTRIBUTING.md
[good-first-issue]: https://github.com/http-rs/async-session/labels/good%20first%20issue
[help-wanted]: https://github.com/http-rs/async-session/labels/help%20wanted

## Acknowledgements

This work is based on the work initiated by
Expand Down
178 changes: 178 additions & 0 deletions docs/en/stores.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Saysion Session Stores

Saysion ships with several pluggable session stores. Most are gated
behind cargo features so you only pull in the dependencies you need.

| Store | Feature flag | Backend |
|---------------------|------------------|------------------|
| `MemoryStore` | *(always on)* | In-process |
| `CookieStore` | *(always on)* | Client cookie |
| `RedisStore` | `redis` | Redis |
| `MongoDbStore` | `mongodb` | MongoDB |
| `SqlxPostgresStore` | `sqlx-postgres` | PostgreSQL |
| `SqlxSqliteStore` | `sqlx-sqlite` | SQLite |
| `SqlxMySqlStore` | `sqlx-mysql` | MySQL / MariaDB |

All stores implement the [`SessionStore`] trait, so they are
interchangeable.

## Redis

```toml
[dependencies]
saysion = { version = "0.1", features = ["redis"] }
```

```rust
use saysion::{RedisStore, Session, SessionStore};

#[tokio::main]
async fn main() -> saysion::Result {
let store = RedisStore::from_url("redis://127.0.0.1:6379")
.await?
.with_prefix("myapp/");

let mut session = Session::new();
session.insert("user_id", 42)?;
let cookie = store.store_session(session).await?.unwrap();

let loaded = store.load_session(cookie).await?.unwrap();
assert_eq!(loaded.get::<i32>("user_id"), Some(42));
Ok(())
}
```

`RedisStore` uses [`redis::aio::ConnectionManager`] internally, which
provides connection pooling and automatic reconnection. Sessions with
an expiry are stored using `SET ... EX <ttl>` so Redis evicts them
automatically.

## MongoDB

```toml
[dependencies]
saysion = { version = "0.1", features = ["mongodb"] }
```

```rust
use saysion::{MongoDbStore, Session, SessionStore};

#[tokio::main]
async fn main() -> saysion::Result {
let store = MongoDbStore::from_uri("mongodb://127.0.0.1:27017", "myapp")
.await?
.with_collection("sessions");

// Create the TTL index. Idempotent — safe to call on every startup.
store.initialize().await?;

let mut session = Session::new();
session.insert("user_id", 42)?;
let cookie = store.store_session(session).await?.unwrap();

let loaded = store.load_session(cookie).await?.unwrap();
assert_eq!(loaded.get::<i32>("user_id"), Some(42));
Ok(())
}
```

The official `mongodb` driver maintains a connection pool internally,
so a single `MongoDbStore` can be cloned and shared. Calling
`initialize()` creates a TTL index on `expires_at` so MongoDB purges
expired sessions automatically.

## SQLx (PostgreSQL / SQLite / MySQL)

Pick the feature(s) for the database(s) you actually use:

```toml
[dependencies]
saysion = { version = "0.1", features = ["sqlx-postgres"] }
# or "sqlx-sqlite", "sqlx-mysql" — multiple are allowed
```

All three SQLx stores share the same API and use a `sqlx::Pool` for
connection pooling.

```rust
use saysion::{Session, SessionStore, SqlxPostgresStore};

#[tokio::main]
async fn main() -> saysion::Result {
let store = SqlxPostgresStore::from_url(
"postgres://postgres:postgres@127.0.0.1/myapp",
)
.await?;

// Create the table on first run.
store.migrate().await?;

let mut session = Session::new();
session.insert("user_id", 42)?;
let cookie = store.store_session(session).await?.unwrap();

let loaded = store.load_session(cookie).await?.unwrap();
assert_eq!(loaded.get::<i32>("user_id"), Some(42));

// Periodically remove expired rows.
store.cleanup().await?;
Ok(())
}
```

For SQLite and MySQL, swap the type and connection URL:

```rust
let store = saysion::SqlxSqliteStore::from_url("sqlite://sessions.db?mode=rwc").await?;
let store = saysion::SqlxMySqlStore::from_url("mysql://root:root@127.0.0.1/myapp").await?;
```

### Schema

All three SQLx stores use the same schema (table name configurable
via `with_table`, defaults to `saysion_sessions`):

| Column | Type | Notes |
|-----------|-------------------------------|--------------------------------|
| `id` | `TEXT` / `VARCHAR(128)` PK | Hashed session id |
| `expires` | `BIGINT` / `INTEGER` nullable | Unix seconds, `NULL` = forever |
| `session` | `TEXT` | JSON-serialized `Session` |

Storing the expiry as a unix-second integer keeps queries portable
across all three databases without pulling in extra type-conversion
features.

## Sharing a store between handlers

All stores are `Clone` and cheap to clone (they hold an internal pool
or connection manager), so wrapping them in an `Arc` is unnecessary —
just clone into your application state.

## Running integration tests

The crate ships integration tests for every backend. They are marked
`#[ignore]` so they will not run unless you opt in:

```bash
# Redis
REDIS_URL=redis://127.0.0.1:6379 \
cargo test --features redis -- --ignored

# MongoDB
MONGODB_URL=mongodb://127.0.0.1:27017 \
cargo test --features mongodb -- --ignored

# PostgreSQL
POSTGRES_URL=postgres://postgres:postgres@127.0.0.1/saysion_test \
cargo test --features sqlx-postgres -- --ignored

# SQLite (no service required)
cargo test --features sqlx-sqlite -- --ignored

# MySQL
MYSQL_URL=mysql://root:root@127.0.0.1/saysion_test \
cargo test --features sqlx-mysql -- --ignored
```

[`SessionStore`]: https://docs.rs/saysion/latest/saysion/trait.SessionStore.html
[`redis::aio::ConnectionManager`]: https://docs.rs/redis/latest/redis/aio/struct.ConnectionManager.html
Loading
Loading