By default, Deno provides access to local storage for persistently storing data that survives Deno runtime restarts. The data is stored in a file on the disk. The local storage implementation doesn't support saving data remotely. This problem has been solved by Deno KV, which will be topic of the next book revision.
The local storage (and session storage too) is internally supported by SQLite. The users would never know or see SQLite. For them, it is a simple & local KV storage. The local storage mechanism is a simple KV store that reside on the disk.
An overview of the local storage architecture is as follows:
The user applications call the local storage APIs provided by Deno. The local storage APIs in JS space are supported by the OPs in rust space. The local storage APIs in rust, in turn, calls the SQLite APIs, which works with the database file on the disk.
In this section, we'll learn:
The location of the database
The queries used to work with the database
For all the interactions with the SQLite database, Deno uses rusqlite crate.
Location of the database
The default location of the SQLite database that would hold the data for local storage is:
On linux, the location of database is $HOME/.cache/deno
On Windows, the location of database is %LOCALAPPDATA%/deno
On Mac, the location of database is $HOME/Library/Caches/deno
In short, the location of the database is inside the cache folder that is used by Deno for other purposes.
Unless used at least once, Deno doesn't create any database file. There is no point in creating database files on the disk if the applications on that server have never or are never going to use it.
~/Library/Caches/deno/location_data: ls -l
total 0
As soon as any of the storage API is called, Deno ensures that a database exists. This is achieved through the following function (get_webstorage):
fnget_webstorage( state:&mutOpState, 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); {letmut 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()?; {letmut 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)}
This function takes the following actions:
Creates a database file
Establish a connection to the database
Apply some default settings
Create a table in the database
The default name for the table is 'data'. This table contains exactly two columns:
key
value
Both key and values are strings, as suggested by the web storage standards. The table is created only if it doesn't exist. If it exists, the table is left untouched. This ensures that the data present in the table remains intact.
As soon as any of the local storage APIs are used, we'll see database files in the storage directories. If we use repl, there will be a storage directory for repl that gets used for all repl sessions.
Now, there is a directory that holds the database files (there are more than one).
~/Library/Caches/deno/location_data: ls -ltrtotal0drwxr-xr-x5mayankcstaff160Jan2718:1192dc35f73a8a47b9f4df517ad0744dd596ae537e9608d33461eb99891fd28f14~/Library/Caches/deno/location_data: ls -l 92dc35f73a8a47b9f4df517ad0744dd596ae537e9608d33461eb99891fd28f14/total104-rw-r--r--1mayankcstaff4096Jan2718:11local_storage-rw-r--r--1mayankcstaff32768Jan2718:11local_storage-shm-rw-r--r--1mayankcstaff12392Jan2718:11local_storage-wal
We can set some item in the local storage for repl, and try to get it in another repl session.
The localStorage read-only property of the window interface allows you to access a Storage object for the Document's origin; the stored data is saved across browser sessions.
This means that the local storage is tied to the origin of the URL in the browser's address bar. This also means that there is separate storage available for each origin.
Deno takes the same concept forward and makes a different database available to different origins. However, there is no concept of address bar here. Therefore, Deno takes origin from two sources:
The main program name
The --location argument if specified
This ensures that each application has their own database.
// storageTest.js
const d = localStorage.getItem("k1");
console.log(d);
// Test
$ deno run storageTest.js
null
// Storage directory
~/Library/Caches/deno/location_data: ls -l
total 0
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:28 92dc35f73a8a47b9f4df517ad0744dd596ae537e9608d33461eb99891fd28f14
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:29 ccd539c6c7447c6d3830b2816f029f5dda4a347e383a608fa7fef00e36432b29
As we can see, in addition to repl database, there is a new one specifically created for this application. A new database gets created for each application that attempts to use local storage.
// storageTest1.js
const d = localStorage.getItem("k1");
console.log(d);
// Test
$ deno run storageTest1.js
null
// Storage directory
~/Library/Caches/deno/location_data: ls -l
total 0
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:31 845857c5ba27e9912cfc1de8b6f8261b86fd13e857aa12f044d59895ebac4ccd
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:28 92dc35f73a8a47b9f4df517ad0744dd596ae537e9608d33461eb99891fd28f14
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:29 ccd539c6c7447c6d3830b2816f029f5dda4a347e383a608fa7fef00e36432b29
Within an application, there is a way to segregate data further by specifying the --location argument. This creates a separate database for each location, which is separate from the database that gets created if location is not specified. Each application can open and use a different database by utilizing the --location argument.
// Using location
$ deno run --location https://deno.com storageTest.js
null
// Storage directory
~/Library/Caches/deno/location_data: ls -l
total 0
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:31 845857c5ba27e9912cfc1de8b6f8261b86fd13e857aa12f044d59895ebac4ccd
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:28 92dc35f73a8a47b9f4df517ad0744dd596ae537e9608d33461eb99891fd28f14
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:29 ccd539c6c7447c6d3830b2816f029f5dda4a347e383a608fa7fef00e36432b29
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:32 d1e195fe7f09a0f7760e7ebbe4e23ac702228747ddf77b771b3055c3e7e5d29f
// Using a different location
$ deno run --location https://denodeploy.com storageTest.js
null
// Storage directory
~/Library/Caches/deno/location_data: ls -l
total 0
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:31 845857c5ba27e9912cfc1de8b6f8261b86fd13e857aa12f044d59895ebac4ccd
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:32 90a9a993d66b0c1d647ea90e4039ebd4733927f43fa583c3636f5a338be65a3e
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:28 92dc35f73a8a47b9f4df517ad0744dd596ae537e9608d33461eb99891fd28f14
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:29 ccd539c6c7447c6d3830b2816f029f5dda4a347e383a608fa7fef00e36432b29
drwxr-xr-x 3 mayankc staff 96 Jan 27 18:32 d1e195fe7f09a0f7760e7ebbe4e23ac702228747ddf77b771b3055c3e7e5d29f
That's all about the database files. We'll now turn our attention to the internals of a couple of heavily used local storage APIs:
getItem
setItem
Get item from local storage
Getting an item from local storage is one of the two most used local storage APIs. The other one is setItem, which we'll see in the next section. This is a simple API with only one input: Key.
We'll trace the journey starting from the user space.
JS space
The getItem API code is as follows:
getItem(key) {webidl.assertBranded(this, StoragePrototype);constprefix="Failed to execute 'getItem' on 'Storage'";webidl.requiredArguments(arguments.length,1, prefix); key =webidl.converters.DOMString(key, prefix,"Argument 1");returnop_webstorage_get(key,this[_persistent]); }
This is a very simple function that
Validates the presence of input key
Defers the work to a rust OP called op_webstorage_get
Rust space
The OP function called op_webstorage_get is a synchronous function, that runs a query in the SQLite database and sends the result back. Note that the getItem and setItem are synchronous functions.
#[op2]#[string]pubfnop_webstorage_get( state:&mutOpState, #[string] key_name:String, persistent:bool,) ->Result<Option<String>, AnyError> {let conn =get_webstorage(state, persistent)?;letmut stmt = conn.prepare_cached("SELECT value FROM data WHERE key = ?")?;let val = stmt.query_row(params![key_name], |row| row.get(0)).optional()?;Ok(val)}
The code of op_webstorage_get is much simpler than our expectations. This function:
Opens a connection to database (if not already open)
Prepares a query
Executes the query
Return the results back
The important part here is the query, which is:
SELECT value FROM data WHERE key = ?
As simple as it looks, that's all about getting an item from local storage. Simply run a query in the SQLite data and return the result. This is no different from running a query directly (which we'll do shortly).
Set item in the local storage
Setting an item into local storage is the other of the two most used local storage APIs.This is a simple API with two inputs: Key & Value.
Again, we'll trace the journey starting from the user space.
JS space
The setItem API code is as follows:
setItem(key, value) {webidl.assertBranded(this, StoragePrototype);constprefix="Failed to execute 'setItem' on 'Storage'";webidl.requiredArguments(arguments.length,2, prefix); key =webidl.converters.DOMString(key, prefix,"Argument 1"); value =webidl.converters.DOMString(value, prefix,"Argument 2");op_webstorage_set(key, value,this[_persistent]); }
This is a very simple function that
Validates the presence of input key & value
Defers the work to a rust OP called op_webstorage_set
Rust space
The OP function called op_webstorage_set is a synchronous function, that runs a DML operation in the SQLite database. Note that the getItem and setItem are synchronous functions.
#[op2(fast)]pubfnop_webstorage_set( state:&mutOpState, #[string] key:&str, #[string] value:&str, persistent:bool,) ->Result<(), AnyError> {let conn =get_webstorage(state, persistent)?;size_check(key.len() + value.len())?;letmut stmt = conn.prepare_cached("SELECT SUM(pgsize) FROM dbstat WHERE name = 'data'")?;let size:u32= stmt.query_row(params![], |row| row.get(0))?;size_check(size asusize)?;letmut stmt = conn.prepare_cached("INSERT OR REPLACE INTO data (key, value) VALUES (?, ?)")?; stmt.execute(params![key, value])?;Ok(())}
The code of op_webstorage_set is also much simpler than our expectations, but slightly longer than the get function. There are multiple size related checks.
Mirroring browser implementations, Deno enforces a 10MB size limit on local storage per origin. This constraint ensures database integrity and prevents excessive storage consumption. The op_webstorage_set function diligently safeguards database sanity by preventing size thresholds from being exceeded.
This op_webstorage_set function:
Opens a connection to database (if not already open)
Checks if the length of key & value is not more than 10MB
Checks if current database size is less than 10MB
Executes the DML operation to insert or update record in the database
The important part here is the DML statement, which is:
INSERT OR REPLACE INTO data (key, value) VALUES (?, ?)
As simple as it looks, that's all about setting an item into local storage. This API returns nothing back.
Other APIs
Before closing this section, we'll take a quick at a couple of other interesting APIs:
removeItem
clear
The removeItem API removes the record from the database. It issues a DELETE command to make this happen.
removeItem(key) {webidl.assertBranded(this, StoragePrototype);constprefix="Failed to execute 'removeItem' on 'Storage'";webidl.requiredArguments(arguments.length,1, prefix); key =webidl.converters.DOMString(key, prefix,"Argument 1");op_webstorage_remove(key,this[_persistent]); }
#[op2(fast)]pubfnop_webstorage_remove( state:&mutOpState, #[string] key_name:&str, persistent:bool,) ->Result<(), AnyError> {let conn =get_webstorage(state, persistent)?;letmut stmt = conn.prepare_cached("DELETE FROM data WHERE key = ?")?; stmt.execute(params![key_name])?;Ok(())}
Lastly, the clear API removes all records from the table. This API also issues the DELETE command without any inputs.
Fulfilling our earlier promise and satisfying our curiosity, we'll briefly examine the database file within the SQLite shell. The local_storage file, residing within any database folder in the root database directory, can be directly opened using the SQLite shell.
~/Library/Caches/deno/location_data: ls -ltrtotal0drwxr-xr-x5mayankcstaff160Jan2721:3392dc35f73a8a47b9f4df517ad0744dd596ae537e9608d33461eb99891fd28f14~/Library/Caches/deno/location_data: cd 92dc35f73a8a47b9f4df517ad0744dd596ae537e9608d33461eb99891fd28f14/~/Library/Caches/deno/location_data/92dc35f73a8a47b9f4df517ad0744dd596ae537e9608d33461eb99891fd28f14: ls -ltrtotal136-rw-r--r--1mayankcstaff4096Jan2721:33local_storage-rw-r--r--1mayankcstaff32768Jan2721:33local_storage-shm-rw-r--r--1mayankcstaff28872Jan2721:33local_storage-wal
First, we'll set a couple of keys into the local storage using deno repl:
As you can see, this is the same type of SQLite database file that you may have used in the past. We've verified that a table named "data" exists with two keys that we set earlier. We've also verified that the table contains two rows matching the data we set through the Deno REPL.
This concludes our exploration of local storage internals in Deno. The next section is about session storage, building upon the detailed background we've established. The focus on session storage will be concise, leveraging your newly acquired understanding of local storage mechanisms.