char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somdeslop: consolidate public type and registry test utils7325e8c

main
7.0 KiB220 linesraw
1mod registry;
2
3use std::io;
4use std::sync::Arc;
5
6use avril_caps::Capabilities;
7use avril_core::serde_v8::{JsBuffer, ToJsBuffer};
8use avril_core::{Extension, Op, PublicModule};
9use avril_kv::{Decision, Store};
10use tokio::runtime::Handle;
11
12pub use avril_kv::{MAX_BATCH_BYTES, MAX_BATCH_OPS};
13pub use registry::Registry;
14
15struct Shared {
16    handle: Handle,
17    caps: Arc<Capabilities>,
18    registry: Registry,
19}
20
21// redb work runs off the isolate thread.
22fn blocking<T, W>(shared: &Arc<Shared>, work: W) -> impl Future<Output = io::Result<T>> + use<T, W>
23where
24    T: Send + 'static,
25    W: FnOnce(&Shared) -> io::Result<T> + Send + 'static,
26{
27    let task = {
28        let shared = shared.clone();
29        shared.handle.clone().spawn_blocking(move || work(&shared))
30    };
31    async move {
32        task.await
33            .unwrap_or_else(|_| Err(io::Error::other("kv background task stopped")))
34    }
35}
36
37// An op on an open database: `function` splits its arguments into the
38// database id and the work to run against the re-authorized store.
39fn kv_op<A, R, W>(
40    shared: &Arc<Shared>,
41    name: &'static str,
42    write: bool,
43    function: impl Fn(A) -> (u64, W) + 'static,
44) -> Op
45where
46    A: serde::de::DeserializeOwned,
47    R: serde::Serialize + Send + 'static,
48    W: FnOnce(&Store) -> io::Result<R> + Send + 'static,
49{
50    let shared = shared.clone();
51    Op::asynchronous(name, move |args: A| {
52        let (db, work) = function(args);
53        blocking(&shared, move |shared| {
54            shared.registry.with_entry(db, write, work)
55        })
56    })
57}
58
59#[derive(serde::Deserialize)]
60struct ListArgs {
61    db: u64,
62    keyspace: String,
63    start: Option<JsBuffer>,
64    end: Option<JsBuffer>,
65    prefix: Option<JsBuffer>,
66    limit: usize,
67    reverse: bool,
68}
69
70// Internal tagging buffers each op before picking a variant, which strips the
71// typed-array identity that JsBuffer needs; serde_bytes accepts plain bytes.
72#[derive(serde::Deserialize)]
73#[serde(tag = "t", rename_all = "snake_case")]
74enum AtomicOp {
75    Check {
76        ks: String,
77        #[serde(with = "serde_bytes")]
78        key: Vec<u8>,
79        expected: Option<String>,
80    },
81    Set {
82        ks: String,
83        #[serde(with = "serde_bytes")]
84        key: Vec<u8>,
85        #[serde(with = "serde_bytes")]
86        value: Vec<u8>,
87    },
88    Delete {
89        ks: String,
90        #[serde(with = "serde_bytes")]
91        key: Vec<u8>,
92    },
93}
94
95impl From<AtomicOp> for avril_kv::BatchOp {
96    fn from(op: AtomicOp) -> Self {
97        match op {
98            AtomicOp::Check { ks, key, expected } => Self::Check { keyspace: ks, key, expected },
99            AtomicOp::Set { ks, key, value } => Self::Set { keyspace: ks, key, value },
100            AtomicOp::Delete { ks, key } => Self::Delete { keyspace: ks, key },
101        }
102    }
103}
104
105pub fn init(handle: Handle, caps: Arc<Capabilities>) -> Extension {
106    let shared = Arc::new(Shared {
107        handle,
108        caps,
109        registry: Registry::new(),
110    });
111    let open = shared.clone();
112    let close = shared.clone();
113    Extension {
114        name: "ext:kv",
115        dependencies: &["ext:file", "ext:url", "ext:web", "ext:encoding"],
116        ops: vec![
117            Op::asynchronous(
118                "op_kv_open",
119                move |(path, access, create): (String, String, bool)| {
120                    blocking(&open, move |shared| {
121                        shared.registry.open(&shared.caps, &path, &access, create)
122                    })
123                },
124            ),
125            Op::asynchronous("op_kv_close", move |id: u64| {
126                blocking(&close, move |shared| shared.registry.close(id))
127            }),
128            kv_op(&shared, "op_kv_list_keyspaces", false, |db: u64| {
129                (db, |store: &Store| store.read_tx(|tx| tx.list_keyspaces()))
130            }),
131            kv_op(
132                &shared,
133                "op_kv_get_many",
134                false,
135                |(db, keyspace, keys): (u64, String, Vec<JsBuffer>)| {
136                    (db, move |store: &Store| {
137                        store.read_tx(|tx| {
138                            keys.iter()
139                                .map(|key| {
140                                    let entry = tx.get(&keyspace, key)?;
141                                    Ok(entry.map(|(value, stamp)| (ToJsBuffer::from(value), stamp)))
142                                })
143                                .collect::<io::Result<Vec<_>>>()
144                        })
145                    })
146                },
147            ),
148            kv_op(
149                &shared,
150                "op_kv_set",
151                true,
152                |(db, keyspace, key, value): (u64, String, JsBuffer, JsBuffer)| {
153                    (db, move |store: &Store| {
154                        store.write_tx(|tx| tx.put(&keyspace, &key, &value).map(Decision::Commit))
155                    })
156                },
157            ),
158            kv_op(
159                &shared,
160                "op_kv_delete",
161                true,
162                |(db, keyspace, key): (u64, String, JsBuffer)| {
163                    (db, move |store: &Store| {
164                        store.write_tx(|tx| {
165                            tx.delete(&keyspace, &key)?;
166                            Ok(Decision::Commit(()))
167                        })
168                    })
169                },
170            ),
171            kv_op(
172                &shared,
173                "op_kv_clear",
174                true,
175                |(db, keyspace): (u64, String)| {
176                    (db, move |store: &Store| {
177                        store.write_tx(|tx| tx.clear(&keyspace).map(Decision::Commit))
178                    })
179                },
180            ),
181            kv_op(&shared, "op_kv_list", false, |args: ListArgs| {
182                (args.db, move |store: &Store| {
183                    let entries = store.read_tx(|tx| {
184                        tx.list(
185                            &args.keyspace,
186                            args.start.as_deref(),
187                            args.end.as_deref(),
188                            args.prefix.as_deref(),
189                            args.limit,
190                            args.reverse,
191                        )
192                    })?;
193                    Ok(entries
194                        .into_iter()
195                        .map(|(key, value, stamp)| {
196                            (ToJsBuffer::from(key), ToJsBuffer::from(value), stamp)
197                        })
198                        .collect::<Vec<_>>())
199                })
200            }),
201            kv_op(
202                &shared,
203                "op_kv_atomic",
204                true,
205                |(db, ops): (u64, Vec<AtomicOp>)| {
206                    let ops: Vec<avril_kv::BatchOp> = ops.into_iter().map(Into::into).collect();
207                    (db, move |store: &Store| store.atomic_commit(&ops))
208                },
209            ),
210        ],
211        modules: const { &[PUBLIC_MODULES[0].module] },
212        entry_point: Some("ext:kv/main.js"),
213    }
214}
215
216pub const PUBLIC_MODULES: &[PublicModule] =
217    &[avril_core::public_module!("avril:kv", "ext:kv", "main")];
218
219#[cfg(test)]
220mod tests;