Skip to content

Commit 8fc0e4e

Browse files
committed
v8: define 'call_describe_module' + test
1 parent 443d553 commit 8fc0e4e

File tree

7 files changed

+157
-15
lines changed

7 files changed

+157
-15
lines changed

crates/core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ test = ["spacetimedb-commitlog/test", "spacetimedb-datastore/test"]
138138
perfmap = []
139139

140140
[dev-dependencies]
141-
spacetimedb-lib = { path = "../lib", features = ["proptest"] }
141+
spacetimedb-lib = { path = "../lib", features = ["proptest", "test"] }
142142
spacetimedb-sats = { path = "../sats", features = ["proptest"] }
143143
spacetimedb-commitlog = { path = "../commitlog", features = ["test"] }
144144
spacetimedb-datastore = { path = "../datastore/", features = ["test"] }

crates/core/src/host/v8/de.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#![allow(dead_code)]
22

3-
use super::key_cache::{get_or_create_key_cache, KeyCache};
43
use super::error::{exception_already_thrown, ExcResult, ExceptionThrown, ExceptionValue, Throwable, TypeError};
54
use super::from_value::{cast, FromValue};
5+
use super::key_cache::{get_or_create_key_cache, KeyCache};
66
use core::fmt;
77
use core::iter::{repeat_n, RepeatN};
88
use core::marker::PhantomData;

crates/core/src/host/v8/error.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Utilities for error handling when dealing with V8.
22
3-
use v8::{Exception, HandleScope, Local, Value};
3+
use v8::{Exception, HandleScope, Local, TryCatch, Value};
44

55
/// The result of trying to convert a [`Value`] in scope `'scope` to some type `T`.
66
pub(super) type ValueResult<'scope, T> = Result<T, ExceptionValue<'scope>>;
@@ -86,3 +86,61 @@ impl<'scope, T: IntoException<'scope>> Throwable<'scope> for T {
8686
exception_already_thrown()
8787
}
8888
}
89+
90+
/// Either an error outside V8 JS execution, or an exception inside.
91+
#[derive(Debug)]
92+
pub(super) enum ErrorOrException<Exc> {
93+
Err(anyhow::Error),
94+
Exception(Exc),
95+
}
96+
97+
impl<E> From<anyhow::Error> for ErrorOrException<E> {
98+
fn from(e: anyhow::Error) -> Self {
99+
Self::Err(e)
100+
}
101+
}
102+
103+
impl From<ExceptionThrown> for ErrorOrException<ExceptionThrown> {
104+
fn from(e: ExceptionThrown) -> Self {
105+
Self::Exception(e)
106+
}
107+
}
108+
109+
impl From<ErrorOrException<JsError>> for anyhow::Error {
110+
fn from(err: ErrorOrException<JsError>) -> Self {
111+
match err {
112+
ErrorOrException::Err(e) => e,
113+
ErrorOrException::Exception(e) => e.into(),
114+
}
115+
}
116+
}
117+
118+
/// A JS exception turned into an error.
119+
#[derive(thiserror::Error, Debug)]
120+
#[error("js error: {msg:?}")]
121+
pub(super) struct JsError {
122+
msg: String,
123+
}
124+
125+
impl JsError {
126+
/// Turns a caught JS exception in `scope` into a [`JSError`].
127+
fn from_caught(scope: &mut TryCatch<'_, HandleScope<'_>>) -> Self {
128+
let msg = match scope.message() {
129+
Some(msg) => msg.get(scope).to_rust_string_lossy(scope),
130+
None => "unknown error".to_owned(),
131+
};
132+
Self { msg }
133+
}
134+
}
135+
136+
/// Run `body` within a try-catch context and capture any JS exception thrown as a [`JsError`].
137+
pub(super) fn catch_exception<'scope, T>(
138+
scope: &mut HandleScope<'scope>,
139+
body: impl FnOnce(&mut HandleScope<'scope>) -> Result<T, ErrorOrException<ExceptionThrown>>,
140+
) -> Result<T, ErrorOrException<JsError>> {
141+
let scope = &mut TryCatch::new(scope);
142+
body(scope).map_err(|e| match e {
143+
ErrorOrException::Err(e) => ErrorOrException::Err(e),
144+
ErrorOrException::Exception(_) => ErrorOrException::Exception(JsError::from_caught(scope)),
145+
})
146+
}

crates/core/src/host/v8/key_cache.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ pub(super) struct KeyCache {
2222
tag: Option<Global<v8::String>>,
2323
/// The `value` property for sum values in JS.
2424
value: Option<Global<v8::String>>,
25+
/// The `describe_module` property on the global proxy object.
26+
describe_module: Option<Global<v8::String>>,
2527
}
2628

2729
impl KeyCache {
@@ -35,6 +37,11 @@ impl KeyCache {
3537
Self::get_or_create_key(scope, &mut self.value, "value")
3638
}
3739

40+
/// Returns the `describe_module` property name.
41+
pub(super) fn describe_module<'scope>(&mut self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> {
42+
Self::get_or_create_key(scope, &mut self.describe_module, "describe_module")
43+
}
44+
3845
/// Returns an interned string corresponding to `string`
3946
/// and memoizes the creation on the v8 side.
4047
fn get_or_create_key<'scope>(

crates/core/src/host/v8/mod.rs

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1-
use super::module_host::CallReducerParams;
2-
use crate::{
3-
host::{
4-
module_common::{build_common_module_from_raw, ModuleCommon},
5-
module_host::{DynModule, Module, ModuleInfo, ModuleInstance, ModuleRuntime},
6-
Scheduler,
7-
},
8-
module_host_context::ModuleCreationContext,
9-
replica_context::ReplicaContext,
10-
};
1+
#![allow(dead_code)]
2+
3+
use super::module_common::{build_common_module_from_raw, ModuleCommon};
4+
use super::module_host::{CallReducerParams, DynModule, Module, ModuleInfo, ModuleInstance, ModuleRuntime};
5+
use crate::host::v8::error::{exception_already_thrown, Throwable};
6+
use crate::{host::Scheduler, module_host_context::ModuleCreationContext, replica_context::ReplicaContext};
117
use anyhow::anyhow;
8+
use de::deserialize_js;
9+
use error::catch_exception;
10+
use from_value::cast;
11+
use key_cache::get_or_create_key_cache;
1212
use spacetimedb_datastore::locking_tx_datastore::MutTxId;
13+
use spacetimedb_lib::RawModuleDef;
1314
use std::sync::{Arc, LazyLock};
15+
use v8::{Function, HandleScope};
1416

1517
mod de;
1618
mod error;
1719
mod from_value;
20+
mod key_cache;
1821
mod ser;
1922
mod to_value;
20-
mod key_cache;
2123

2224
/// The V8 runtime, for modules written in e.g., JS or TypeScript.
2325
#[derive(Default)]
@@ -131,3 +133,73 @@ impl ModuleInstance for JsInstance {
131133
todo!()
132134
}
133135
}
136+
137+
// Calls the `describe_module` function on the global proxy object to extract a [`RawModuleDef`].
138+
fn call_describe_module(scope: &mut HandleScope<'_>) -> anyhow::Result<RawModuleDef> {
139+
// Get a cached version of the `describe_module` property.
140+
let key_cache = get_or_create_key_cache(scope);
141+
let describe_module_key = key_cache.borrow_mut().describe_module(scope).into();
142+
143+
catch_exception(scope, |scope| {
144+
// Get the function on the global proxy object.
145+
let object = scope
146+
.get_current_context()
147+
.global(scope)
148+
.get(scope, describe_module_key)
149+
.ok_or_else(exception_already_thrown)?;
150+
151+
// Convert to a function.
152+
let fun =
153+
cast!(scope, object, Function, "function export for `describe_module`").map_err(|e| e.throw(scope))?;
154+
155+
// Call the function.
156+
let receiver = v8::undefined(scope).into();
157+
let raw_mod_js = fun.call(scope, receiver, &[]).ok_or_else(exception_already_thrown)?;
158+
159+
// Deserialize the raw module.
160+
let raw_mod: RawModuleDef = deserialize_js(scope, raw_mod_js)?;
161+
Ok(raw_mod)
162+
})
163+
.map_err(Into::into)
164+
}
165+
166+
#[cfg(test)]
167+
mod test {
168+
use super::*;
169+
use crate::host::v8::to_value::test::with_scope;
170+
use v8::{Local, Value};
171+
172+
fn with_script<R>(
173+
code: &str,
174+
logic: impl for<'scope> FnOnce(&mut HandleScope<'scope>, Local<'scope, Value>) -> R,
175+
) -> R {
176+
with_scope(|scope| {
177+
let code = v8::String::new(scope, code).unwrap();
178+
let script_val = v8::Script::compile(scope, code, None).unwrap().run(scope).unwrap();
179+
logic(scope, script_val)
180+
})
181+
}
182+
183+
#[test]
184+
fn call_describe_module_works() {
185+
let code = r#"
186+
function describe_module() {
187+
return {
188+
"tag": "V9",
189+
"value": {
190+
"typespace": {
191+
"types": [],
192+
},
193+
"tables": [],
194+
"reducers": [],
195+
"types": [],
196+
"misc_exports": [],
197+
"row_level_security": [],
198+
},
199+
};
200+
}
201+
"#;
202+
let raw_mod = with_script(code, |scope, _| call_describe_module(scope).unwrap());
203+
assert_eq!(raw_mod, RawModuleDef::V9(<_>::default()));
204+
}
205+
}

crates/lib/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ proptest = { workspace = true, optional = true }
5454
proptest-derive = { workspace = true, optional = true }
5555

5656
[dev-dependencies]
57-
spacetimedb-sats = { path = "../sats", features = ["test"] }
57+
spacetimedb-sats = { workspace = true, features = ["test"] }
5858
bytes.workspace = true
5959
serde_json.workspace = true
6060
insta.workspace = true

crates/lib/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ impl TableDesc {
119119
}
120120

121121
#[derive(Debug, Clone, SpacetimeType)]
122+
#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
122123
#[sats(crate = crate)]
123124
pub struct ReducerDef {
124125
pub name: Box<str>,
@@ -189,6 +190,7 @@ impl_serialize!([] ReducerArgsWithSchema<'_>, (self, ser) => {
189190

190191
//WARNING: Change this structure (or any of their members) is an ABI change.
191192
#[derive(Debug, Clone, Default, SpacetimeType)]
193+
#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
192194
#[sats(crate = crate)]
193195
pub struct RawModuleDefV8 {
194196
pub typespace: sats::Typespace,
@@ -213,6 +215,7 @@ impl RawModuleDefV8 {
213215
///
214216
/// This is what is actually returned by the module when `__describe_module__` is called, serialized to BSATN.
215217
#[derive(Debug, Clone, SpacetimeType)]
218+
#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
216219
#[sats(crate = crate)]
217220
#[non_exhaustive]
218221
pub enum RawModuleDef {
@@ -340,12 +343,14 @@ impl TypespaceBuilder for ModuleDefBuilder {
340343

341344
// an enum to keep it extensible without breaking abi
342345
#[derive(Debug, Clone, SpacetimeType)]
346+
#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
343347
#[sats(crate = crate)]
344348
pub enum MiscModuleExport {
345349
TypeAlias(TypeAlias),
346350
}
347351

348352
#[derive(Debug, Clone, SpacetimeType)]
353+
#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
349354
#[sats(crate = crate)]
350355
pub struct TypeAlias {
351356
pub name: String,

0 commit comments

Comments
 (0)