deno_webstorage/
lib.rs

1// Copyright 2018-2025 the Deno authors. MIT license.
2
3// NOTE to all: use **cached** prepared statements when interfacing with SQLite.
4
5use std::path::PathBuf;
6
7use deno_core::GarbageCollected;
8use deno_core::OpState;
9use deno_core::op2;
10pub use rusqlite;
11use rusqlite::Connection;
12use rusqlite::OptionalExtension;
13use rusqlite::params;
14
15#[derive(Debug, thiserror::Error, deno_error::JsError)]
16pub enum WebStorageError {
17  #[class("DOMExceptionNotSupportedError")]
18  #[error("LocalStorage is not supported in this context.")]
19  ContextNotSupported,
20  #[class(generic)]
21  #[error(transparent)]
22  Sqlite(#[from] rusqlite::Error),
23  #[class(inherit)]
24  #[error(transparent)]
25  Io(std::io::Error),
26  #[class("DOMExceptionQuotaExceededError")]
27  #[error("Exceeded maximum storage size")]
28  StorageExceeded,
29}
30
31#[derive(Clone)]
32struct OriginStorageDir(PathBuf);
33
34const MAX_STORAGE_BYTES: usize = 10 * 1024 * 1024;
35
36deno_core::extension!(deno_webstorage,
37  deps = [ deno_webidl ],
38  ops = [
39    op_webstorage_iterate_keys,
40  ],
41  objects = [
42    Storage
43  ],
44  esm = [ "01_webstorage.js" ],
45  options = {
46      origin_storage_dir: Option<PathBuf>
47  },
48  state = |state, options| {
49    if let Some(origin_storage_dir) = options.origin_storage_dir {
50      state.put(OriginStorageDir(origin_storage_dir));
51    }
52  },
53);
54
55struct LocalStorage(Connection);
56struct SessionStorage(Connection);
57
58fn get_webstorage(
59  state: &mut OpState,
60  persistent: bool,
61) -> Result<&Connection, WebStorageError> {
62  let conn = if persistent {
63    if state.try_borrow::<LocalStorage>().is_none() {
64      let path = state
65        .try_borrow::<OriginStorageDir>()
66        .ok_or(WebStorageError::ContextNotSupported)?;
67      std::fs::create_dir_all(&path.0).map_err(WebStorageError::Io)?;
68      let conn = Connection::open(path.0.join("local_storage"))?;
69      // Enable write-ahead-logging and tweak some other stuff.
70      let initial_pragmas = "
71        -- enable write-ahead-logging mode
72        PRAGMA journal_mode=WAL;
73        PRAGMA synchronous=NORMAL;
74        PRAGMA temp_store=memory;
75        PRAGMA page_size=4096;
76        PRAGMA mmap_size=6000000;
77        PRAGMA optimize;
78      ";
79
80      conn.execute_batch(initial_pragmas)?;
81      conn.set_prepared_statement_cache_capacity(128);
82      {
83        let mut stmt = conn.prepare_cached(
84          "CREATE TABLE IF NOT EXISTS data (key VARCHAR UNIQUE, value VARCHAR)",
85        )?;
86        stmt.execute(params![])?;
87      }
88      state.put(LocalStorage(conn));
89    }
90
91    &state.borrow::<LocalStorage>().0
92  } else {
93    if state.try_borrow::<SessionStorage>().is_none() {
94      let conn = Connection::open_in_memory()?;
95      {
96        let mut stmt = conn.prepare_cached(
97          "CREATE TABLE data (key VARCHAR UNIQUE, value VARCHAR)",
98        )?;
99        stmt.execute(params![])?;
100      }
101      state.put(SessionStorage(conn));
102    }
103
104    &state.borrow::<SessionStorage>().0
105  };
106
107  Ok(conn)
108}
109
110#[inline]
111fn size_check(input: usize) -> Result<(), WebStorageError> {
112  if input >= MAX_STORAGE_BYTES {
113    return Err(WebStorageError::StorageExceeded);
114  }
115
116  Ok(())
117}
118
119struct Storage {
120  persistent: bool,
121}
122
123// SAFETY: we're sure this can be GCed
124unsafe impl GarbageCollected for Storage {
125  fn trace(&self, _visitor: &mut deno_core::v8::cppgc::Visitor) {}
126
127  fn get_name(&self) -> &'static std::ffi::CStr {
128    c"Storage"
129  }
130}
131
132#[op2]
133impl Storage {
134  #[constructor]
135  #[cppgc]
136  fn new(persistent: bool) -> Storage {
137    Storage { persistent }
138  }
139
140  #[getter]
141  #[smi]
142  fn length(&self, state: &mut OpState) -> Result<u32, WebStorageError> {
143    let conn = get_webstorage(state, self.persistent)?;
144
145    let mut stmt = conn.prepare_cached("SELECT COUNT(*) FROM data")?;
146    let length: u32 = stmt.query_row(params![], |row| row.get(0))?;
147
148    Ok(length)
149  }
150
151  #[required(1)]
152  #[string]
153  fn key(
154    &self,
155    state: &mut OpState,
156    #[smi] index: u32,
157  ) -> Result<Option<String>, WebStorageError> {
158    let conn = get_webstorage(state, self.persistent)?;
159
160    let mut stmt =
161      conn.prepare_cached("SELECT key FROM data LIMIT 1 OFFSET ?")?;
162
163    let key: Option<String> = stmt
164      .query_row(params![index], |row| row.get(0))
165      .optional()?;
166
167    Ok(key)
168  }
169
170  #[fast]
171  #[required(2)]
172  fn set_item(
173    &self,
174    state: &mut OpState,
175    #[string] key: &str,
176    #[string] value: &str,
177  ) -> Result<(), WebStorageError> {
178    let conn = get_webstorage(state, self.persistent)?;
179
180    size_check(key.len() + value.len())?;
181
182    let mut stmt = conn
183      .prepare_cached("SELECT SUM(pgsize) FROM dbstat WHERE name = 'data'")?;
184    let size: u32 = stmt.query_row(params![], |row| row.get(0))?;
185
186    size_check(size as usize)?;
187
188    let mut stmt = conn.prepare_cached(
189      "INSERT OR REPLACE INTO data (key, value) VALUES (?, ?)",
190    )?;
191    stmt.execute(params![key, value])?;
192
193    Ok(())
194  }
195
196  #[required(1)]
197  #[string]
198  fn get_item(
199    &self,
200    state: &mut OpState,
201    #[string] key: &str,
202  ) -> Result<Option<String>, WebStorageError> {
203    let conn = get_webstorage(state, self.persistent)?;
204
205    let mut stmt =
206      conn.prepare_cached("SELECT value FROM data WHERE key = ?")?;
207    let val = stmt.query_row(params![key], |row| row.get(0)).optional()?;
208
209    Ok(val)
210  }
211
212  #[fast]
213  #[required(1)]
214  fn remove_item(
215    &self,
216    state: &mut OpState,
217    #[string] key: &str,
218  ) -> Result<(), WebStorageError> {
219    let conn = get_webstorage(state, self.persistent)?;
220
221    let mut stmt = conn.prepare_cached("DELETE FROM data WHERE key = ?")?;
222    stmt.execute(params![key])?;
223
224    Ok(())
225  }
226
227  #[fast]
228  fn clear(&self, state: &mut OpState) -> Result<(), WebStorageError> {
229    let conn = get_webstorage(state, self.persistent)?;
230
231    let mut stmt = conn.prepare_cached("DELETE FROM data")?;
232    stmt.execute(params![])?;
233
234    Ok(())
235  }
236}
237
238#[op2]
239#[serde]
240fn op_webstorage_iterate_keys(
241  #[cppgc] storage: &Storage,
242  state: &mut OpState,
243) -> Result<Vec<String>, WebStorageError> {
244  let conn = get_webstorage(state, storage.persistent)?;
245
246  let mut stmt = conn.prepare_cached("SELECT key FROM data")?;
247  let keys = stmt
248    .query_map(params![], |row| row.get::<_, String>(0))?
249    .map(|r| r.unwrap())
250    .collect();
251
252  Ok(keys)
253}