7.3 Session storage

Session storage is just like local storage, except for the data persistence across Deno runtime restarts. The session storage mechanism uses SQLite's in-memory database feature. The SQLite's in-memory database feature is (credit: SQLite documentation):
An SQLite database is normally stored in a single ordinary disk file. However, in certain circumstances, the database might be stored in memory.
The most common way to force an SQLite database to exist purely in memory is to open the database using the special filename ":memory:". In other words, instead of passing the name of a real disk file into one of the sqlite3_open(), sqlite3_open16(), or sqlite3_open_v2() functions, pass in the string ":memory:". For example:
rc = sqlite3_open(":memory:", &db);
When this is done, no disk file is opened. Instead, a new database is created purely in memory. The database ceases to exist as soon as the database connection is closed. Every :memory: database is distinct from every other. So, opening two database connections each with the filename ":memory:" will create two independent in-memory databases.
The transient nature of SQLite's in-memory database perfectly matches the requirements of web standard's session storage.
An overview of the session storage architecture is as follows:
The user applications call the session storage APIs provided by Deno. The session storage APIs in JS space are supported by the OPs in the rust space. The session storage APIs in rust, in turn, calls the SQLite APIs, which works with the in-memory database.
The primary difference between local storage and session storage is how the database is opened (in-memory or file). Once opened, all the APIs and database queries are the same. As all the APIs are the same, we'll take a detailed look at the opening of the database rather than the APIs and queries that we've already seen in local storage. We'll still look at internals of the one of the API: length.

Opening of database

As we understand that, the session storage mechanisms open the SQLite database using a special path called ':memory:'. This indicates SQLite to open a temporary database in memory that should cease to exist as soon as the database connection is closed.
You would have noticed that there is a flag called 'persistent' in all the OPs related to storage APIs. This flag determines whether the storage is transient or persistent.
let localStorageStorage;
function localStorage() {
if (!localStorageStorage) {
localStorageStorage = createStorage(true);
}
return localStorageStorage;
}
let sessionStorageStorage;
function sessionStorage() {
if (!sessionStorageStorage) {
sessionStorageStorage = createStorage(false);
}
return sessionStorageStorage;
}
Deno creates both storage the same way, except for their nature: persistent or not.
Let's take a look at the get_storage function again:
fn get_webstorage(
state: &mut OpState,
persistent: bool,
) -> Result<&Connection, AnyError> {
let conn = if persistent {
if state.try_borrow::<LocalStorage>().is_none() {
let path = state.try_borrow::<OriginStorageDir>().ok_or_else(|| {
DomExceptionNotSupportedError::new(
"LocalStorage is not supported in this context.",
)
})?;
std::fs::create_dir_all(&path.0)?;
let conn = Connection::open(path.0.join("local_storage"))?;
// Enable write-ahead-logging and tweak some other stuff.
let initial_pragmas = "
-- enable write-ahead-logging mode
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA temp_store=memory;
PRAGMA page_size=4096;
PRAGMA mmap_size=6000000;
PRAGMA optimize;
";
conn.execute_batch(initial_pragmas)?;
conn.set_prepared_statement_cache_capacity(128);
{
let mut stmt = conn.prepare_cached(
"CREATE TABLE IF NOT EXISTS data (key VARCHAR UNIQUE, value VARCHAR)",
)?;
stmt.execute(params![])?;
}
state.put(LocalStorage(conn));
}
&state.borrow::<LocalStorage>().0
} else {
if state.try_borrow::<SessionStorage>().is_none() {
let conn = Connection::open_in_memory()?;
{
let mut stmt = conn.prepare_cached(
"CREATE TABLE data (key VARCHAR UNIQUE, value VARCHAR)",
)?;
stmt.execute(params![])?;
}
state.put(SessionStorage(conn));
}
&state.borrow::<SessionStorage>().0
};
Ok(conn)
}
The relevant part of this function is the bottom 'else' part where, Deno uses rusqlite's open_in_memory API to create an in-memory database. This makes it interesting to confirm how rusqlite implements the open_in_memory API. The following code is from rusqlite:
#[inline]
pub fn open_in_memory() -> Result<Connection> {
let flags = OpenFlags::default();
Connection::open_in_memory_with_flags(flags)
}
#[inline]
pub fn open_in_memory_with_flags(flags: OpenFlags) -> Result<Connection> {
Connection::open_with_flags(":memory:", flags)
}
As we can see that, the rusqlite API opens the SQLite database connection using a special path, ':memory:". The following diagram makes the distinction clearer:
Once the database connection is opened, all the subsequent APIs and queries are the same. We'll still take a look at one of the API we haven't seen earlier: length.

Getting number of items in database

The length read-only property of the Storage interface returns the number of data items stored in a given Storage object.
JS space
get length() {
webidl.assertBranded(this, StoragePrototype);
return op_webstorage_length(this[_persistent]);
}
Rust space
#[op2(fast)]
pub fn op_webstorage_length(
state: &mut OpState,
persistent: bool,
) -> Result<u32, AnyError> {
let conn = get_webstorage(state, persistent)?;
let mut stmt = conn.prepare_cached("SELECT COUNT(*) FROM data")?;
let length: u32 = stmt.query_row(params![], |row| row.get(0))?;
Ok(length)
}
The query executed to fetch the number of records is COUNT(*). This query works on a SQLite database whether it is in-memory or on a disk.

That's all about the internals of session storage.