Skip to content

Commit ab7e5d0

Browse files
committed
Implement calling views
1 parent a9f3444 commit ab7e5d0

File tree

5 files changed

+218
-9
lines changed

5 files changed

+218
-9
lines changed

crates/core/src/host/module_host.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ impl Instance {
408408
fn call_view(&mut self, tx: MutTxId, params: CallViewParams) -> ViewCallResult {
409409
match self {
410410
Instance::Wasm(inst) => inst.call_view(tx, params),
411-
Instance::Js(_inst) => unimplemented!("JS views are not implemented yet"),
411+
Instance::Js(inst) => inst.call_view(tx, params),
412412
}
413413
}
414414

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

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ use self::error::{
55
};
66
use self::ser::serialize_to_js;
77
use self::string::{str_from_ident, IntoJsString};
8-
use self::syscall::{call_call_reducer, call_describe_module, get_hooks, resolve_sys_module, FnRet, HookFunctions};
8+
use self::syscall::{
9+
call_call_reducer, call_call_view, call_call_view_anon, call_describe_module, get_hooks, resolve_sys_module, FnRet,
10+
HookFunctions,
11+
};
912
use super::module_common::{build_common_module_from_raw, run_describer, ModuleCommon};
1013
use super::module_host::{CallProcedureParams, CallReducerParams, Module, ModuleInfo, ModuleRuntime};
1114
use super::UpdateDatabaseResult;
1215
use crate::host::instance_env::{ChunkPool, InstanceEnv};
13-
use crate::host::module_host::Instance;
16+
use crate::host::module_host::{CallViewParams, Instance, ViewCallResult};
1417
use crate::host::v8::error::{ErrorOrException, ExceptionThrown};
1518
use crate::host::wasm_common::instrumentation::CallTimes;
1619
use crate::host::wasm_common::module_host_actor::{
17-
DescribeError, ExecutionStats, ExecutionTimings, InstanceCommon, ReducerExecuteResult,
20+
DescribeError, ExecutionStats, ExecutionTimings, InstanceCommon, ReducerExecuteResult, ViewExecuteResult,
1821
};
1922
use crate::host::wasm_common::{RowIters, TimingSpanSet};
2023
use crate::host::{ReducerCallResult, Scheduler};
@@ -25,7 +28,7 @@ use anyhow::Context as _;
2528
use core::str;
2629
use itertools::Either;
2730
use spacetimedb_client_api_messages::energy::FunctionBudget;
28-
use spacetimedb_datastore::locking_tx_datastore::{FuncCallType, MutTxId};
31+
use spacetimedb_datastore::locking_tx_datastore::{FuncCallType, MutTxId, UniqueView};
2932
use spacetimedb_datastore::traits::Program;
3033
use spacetimedb_lib::{RawModuleDef, Timestamp};
3134
use spacetimedb_schema::auto_migrate::MigrationPolicy;
@@ -292,6 +295,14 @@ impl JsInstance {
292295
) -> Result<super::ProcedureCallResult, super::ProcedureCallError> {
293296
todo!("JS/TS module procedure support")
294297
}
298+
299+
pub fn call_view(&mut self, tx: MutTxId, params: CallViewParams) -> ViewCallResult {
300+
let (response, trapped) = self.call(|response| JsWorkerRequest::CallView { tx, params, response });
301+
302+
self.trapped = trapped;
303+
304+
response
305+
}
295306
}
296307

297308
/// A request for the worker in [`spawn_instance_worker`].
@@ -312,6 +323,12 @@ enum JsWorkerRequest {
312323
params: CallReducerParams,
313324
response: oneshot::Sender<(ReducerCallResult, bool)>,
314325
},
326+
/// See [`JsInstance::call_view`].
327+
CallView {
328+
tx: MutTxId,
329+
params: CallViewParams,
330+
response: oneshot::Sender<(ViewCallResult, bool)>,
331+
},
315332
}
316333

317334
/// Performs some of the startup work of [`spawn_instance_worker`].
@@ -443,6 +460,13 @@ fn spawn_instance_worker(
443460
unreachable!("should have receiver for `call_reducer` response");
444461
}
445462
}
463+
JsWorkerRequest::CallView { tx, params, response } => {
464+
let res = call_view(&mut instance_common, replica_ctx, scope, &hooks, tx, params);
465+
466+
if let Err(_e) = response.send(res) {
467+
unreachable!("should have receiver for `call_view` response");
468+
}
469+
}
446470
}
447471
}
448472
});
@@ -647,6 +671,43 @@ fn call_reducer<'scope>(
647671
(res, trapped)
648672
}
649673

674+
fn call_view<'scope>(
675+
instance_common: &mut InstanceCommon,
676+
replica_ctx: &ReplicaContext,
677+
scope: &mut PinScope<'scope, '_>,
678+
hooks: &HookFunctions<'_>,
679+
tx: MutTxId,
680+
params: CallViewParams,
681+
) -> (ViewCallResult, bool) {
682+
let mut trapped = false;
683+
684+
let is_anonymous = params.is_anonymous;
685+
let (res, _) = instance_common.call_view_with_tx(
686+
replica_ctx,
687+
tx,
688+
params,
689+
move |a, b, c| log_traceback(replica_ctx, a, b, c),
690+
|tx, op, budget| {
691+
let func = FuncCallType::View(if is_anonymous {
692+
UniqueView::anonymous(op.id, op.args.get_bsatn().clone())
693+
} else {
694+
UniqueView::with_identity(*op.caller_identity, op.id, op.args.get_bsatn().clone())
695+
});
696+
let (tx, stats, call_result) =
697+
common_call(scope, tx, op.name, op.timestamp, budget, func, &mut trapped, |scope| {
698+
Ok(if is_anonymous {
699+
call_call_view_anon(scope, hooks, op.into())?
700+
} else {
701+
call_call_view(scope, hooks, op)?
702+
})
703+
});
704+
(tx, ViewExecuteResult { stats, call_result })
705+
},
706+
);
707+
708+
(res, trapped)
709+
}
710+
650711
/// Extracts the raw module def by running the registered `__describe_module__` hook.
651712
fn extract_description<'scope>(
652713
scope: &mut PinScope<'scope, '_>,

crates/core/src/host/v8/syscall/hooks.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ pub(super) fn set_hook_slots(
4848
pub(in super::super) enum ModuleHookKey {
4949
DescribeModule,
5050
CallReducer,
51+
CallView,
52+
CallAnonymousView,
5153
}
5254

5355
impl ModuleHookKey {
@@ -59,6 +61,8 @@ impl ModuleHookKey {
5961
// reverted to just 0, 1... once denoland/rusty_v8#1868 merges
6062
ModuleHookKey::DescribeModule => 20,
6163
ModuleHookKey::CallReducer => 21,
64+
ModuleHookKey::CallView => 22,
65+
ModuleHookKey::CallAnonymousView => 23,
6266
}
6367
}
6468
}
@@ -104,6 +108,8 @@ pub(in super::super) struct HookFunctions<'scope> {
104108
/// describe_module and call_reducer existed in v1.0, but everything else is `Option`al
105109
pub describe_module: Local<'scope, Function>,
106110
pub call_reducer: Local<'scope, Function>,
111+
pub call_view: Option<Local<'scope, Function>>,
112+
pub call_view_anon: Option<Local<'scope, Function>>,
107113
}
108114

109115
/// Returns the hook function previously registered in [`register_hooks`].
@@ -123,5 +129,7 @@ pub(in super::super) fn get_hooks<'scope>(scope: &mut PinScope<'scope, '_>) -> O
123129
abi: hooks.abi,
124130
describe_module: get(ModuleHookKey::DescribeModule)?,
125131
call_reducer: get(ModuleHookKey::CallReducer)?,
132+
call_view: get(ModuleHookKey::CallView),
133+
call_view_anon: get(ModuleHookKey::CallAnonymousView),
126134
})
127135
}

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
use bytes::Bytes;
12
use spacetimedb_lib::{RawModuleDef, VersionTuple};
23
use v8::{callback_scope, Context, FixedArray, Local, Module, PinScope};
34

45
use crate::host::v8::de::scratch_buf;
56
use crate::host::v8::error::{ErrorOrException, ExcResult, ExceptionThrown, Throwable, TypeError};
67
use crate::host::wasm_common::abi::parse_abi_version;
7-
use crate::host::wasm_common::module_host_actor::{ReducerOp, ReducerResult};
8+
use crate::host::wasm_common::module_host_actor::{AnonymousViewOp, ReducerOp, ReducerResult, ViewOp};
89

910
mod hooks;
1011
mod v1;
@@ -52,6 +53,7 @@ fn resolve_sys_module_inner<'scope>(
5253
match module {
5354
"sys" => match (major, minor) {
5455
(1, 0) => Ok(v1::sys_v1_0(scope)),
56+
(1, 1) => Ok(v1::sys_v1_1(scope)),
5557
_ => Err(TypeError(format!(
5658
"Could not import {spec:?}, likely because this module was built for a newer version of SpacetimeDB.\n\
5759
It requires sys module v{major}.{minor}, but that version is not supported by the database."
@@ -75,6 +77,32 @@ pub(super) fn call_call_reducer(
7577
}
7678
}
7779

80+
/// Calls the registered `__call_view__` function hook.
81+
///
82+
/// This handles any (future) ABI version differences.
83+
pub(super) fn call_call_view(
84+
scope: &mut PinScope<'_, '_>,
85+
hooks: &HookFunctions<'_>,
86+
op: ViewOp<'_>,
87+
) -> Result<Bytes, ErrorOrException<ExceptionThrown>> {
88+
match hooks.abi {
89+
AbiVersion::V1 => v1::call_call_view(scope, hooks, op),
90+
}
91+
}
92+
93+
/// Calls the registered `__call_view_anon__` function hook.
94+
///
95+
/// This handles any (future) ABI version differences.
96+
pub(super) fn call_call_view_anon(
97+
scope: &mut PinScope<'_, '_>,
98+
hooks: &HookFunctions<'_>,
99+
op: AnonymousViewOp<'_>,
100+
) -> Result<Bytes, ErrorOrException<ExceptionThrown>> {
101+
match hooks.abi {
102+
AbiVersion::V1 => v1::call_call_view_anon(scope, hooks, op),
103+
}
104+
}
105+
78106
/// Calls the registered `__describe_module__` function hook.
79107
///
80108
/// This handles any (future) ABI version differences.

crates/core/src/host/v8/syscall/v1.rs

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,21 @@ use crate::host::v8::{
1414
TerminationError, Throwable,
1515
};
1616
use crate::host::wasm_common::instrumentation::span;
17-
use crate::host::wasm_common::module_host_actor::{ReducerOp, ReducerResult};
17+
use crate::host::wasm_common::module_host_actor::{AnonymousViewOp, ReducerOp, ReducerResult, ViewOp};
1818
use crate::host::wasm_common::{err_to_errno_and_log, RowIterIdx, TimingSpan, TimingSpanIdx};
1919
use crate::host::AbiCall;
2020
use anyhow::Context;
21+
use bytes::Bytes;
2122
use spacetimedb_lib::{bsatn, ConnectionId, Identity, RawModuleDef};
22-
use spacetimedb_primitives::{errno, ColId, IndexId, ReducerId, TableId};
23+
use spacetimedb_primitives::{errno, ColId, IndexId, ReducerId, TableId, ViewId};
2324
use spacetimedb_sats::Serialize;
2425
use v8::{
2526
callback_scope, ConstructorBehavior, Function, FunctionCallbackArguments, Isolate, Local, Module, Object,
2627
PinCallbackScope, PinScope,
2728
};
2829

2930
macro_rules! create_synthetic_module {
30-
($scope:expr, $module_name:expr, $(($wrapper:ident, $abi_call:expr, $fun:ident),)*) => {{
31+
($scope:expr, $module_name:expr $(, ($wrapper:ident, $abi_call:expr, $fun:ident))* $(,)?) => {{
3132
let export_names = &[$(str_from_ident!($fun).string($scope)),*];
3233
let eval_steps = |context, module| {
3334
callback_scope!(unsafe scope, context);
@@ -112,6 +113,11 @@ pub(super) fn sys_v1_0<'scope>(scope: &mut PinScope<'scope, '_>) -> Local<'scope
112113
)
113114
}
114115

116+
pub(super) fn sys_v1_1<'scope>(scope: &mut PinScope<'scope, '_>) -> Local<'scope, Module> {
117+
use register_hooks_v1_1 as register_hooks;
118+
create_synthetic_module!(scope, "spacetime:sys@1.1", (with_nothing, (), register_hooks))
119+
}
120+
115121
/// Registers a function in `module`
116122
/// where the function has `name` and does `body`.
117123
fn register_module_fun(
@@ -333,6 +339,51 @@ fn register_hooks_v1_0<'scope>(scope: &mut PinScope<'scope, '_>, args: FunctionC
333339
Ok(v8::undefined(scope).into())
334340
}
335341

342+
/// Module ABI that registers the functions called by the host.
343+
///
344+
/// # Signature
345+
///
346+
/// ```ignore
347+
/// register_hooks(hooks: {
348+
/// __call_view__(view_id: u32, sender: u256, args: u8[]): u8[];
349+
/// __call_view_anon__(view_id: u32, args: u8[]): u8[];
350+
/// }): void
351+
/// ```
352+
///
353+
/// # Types
354+
///
355+
/// - `u8` is `number` in JS restricted to unsigned 8-bit integers.
356+
/// - `u32` is `bigint` in JS restricted to unsigned 32-bit integers.
357+
/// - `u256` is `bigint` in JS restricted to unsigned 256-bit integers.
358+
///
359+
/// # Returns
360+
///
361+
/// Returns nothing.
362+
///
363+
/// # Throws
364+
///
365+
/// Throws a `TypeError` if:
366+
/// - `hooks` is not an object that has the correct functions.
367+
fn register_hooks_v1_1<'scope>(scope: &mut PinScope<'scope, '_>, args: FunctionCallbackArguments<'_>) -> FnRet<'scope> {
368+
// Convert `hooks` to an object.
369+
let hooks = cast!(scope, args.get(0), Object, "hooks object").map_err(|e| e.throw(scope))?;
370+
371+
let call_view = get_hook_function(scope, hooks, str_from_ident!(__call_view__))?;
372+
let call_anonymous_view = get_hook_function(scope, hooks, str_from_ident!(__call_anonymous_view__))?;
373+
374+
// Set the hooks.
375+
set_hook_slots(
376+
scope,
377+
AbiVersion::V1,
378+
&[
379+
(ModuleHookKey::CallView, call_view),
380+
(ModuleHookKey::CallAnonymousView, call_anonymous_view),
381+
],
382+
)?;
383+
384+
Ok(v8::undefined(scope).into())
385+
}
386+
336387
/// Calls the `__call_reducer__` function `fun`.
337388
pub(super) fn call_call_reducer(
338389
scope: &mut PinScope<'_, '_>,
@@ -364,6 +415,67 @@ pub(super) fn call_call_reducer(
364415
Ok(user_res)
365416
}
366417

418+
/// Calls the `__call_view__` function `fun`.
419+
pub(super) fn call_call_view(
420+
scope: &mut PinScope<'_, '_>,
421+
hooks: &HookFunctions<'_>,
422+
op: ViewOp<'_>,
423+
) -> Result<Bytes, ErrorOrException<ExceptionThrown>> {
424+
let fun = hooks.call_view.context("`__call_view__` was never defined")?;
425+
426+
let ViewOp {
427+
id: ViewId(view_id),
428+
name: _,
429+
caller_identity: sender,
430+
timestamp: _,
431+
args: view_args,
432+
} = op;
433+
// Serialize the arguments.
434+
let view_id = serialize_to_js(scope, &view_id)?;
435+
let sender = serialize_to_js(scope, &sender.to_u256())?;
436+
let view_args = serialize_to_js(scope, view_args.get_bsatn())?;
437+
let args = &[view_id, sender, view_args];
438+
439+
// Call the function.
440+
let ret = call_free_fun(scope, fun, args)?;
441+
442+
// Deserialize the user result.
443+
let ret = cast!(scope, ret, v8::Uint8Array, "bytes return from `__call_view__`").map_err(|e| e.throw(scope))?;
444+
let bytes = ret.get_contents(&mut []);
445+
446+
Ok(Bytes::copy_from_slice(bytes))
447+
}
448+
449+
/// Calls the `__call_view_anon__` function `fun`.
450+
pub(super) fn call_call_view_anon(
451+
scope: &mut PinScope<'_, '_>,
452+
hooks: &HookFunctions<'_>,
453+
op: AnonymousViewOp<'_>,
454+
) -> Result<Bytes, ErrorOrException<ExceptionThrown>> {
455+
let fun = hooks.call_view_anon.context("`__call_view__` was never defined")?;
456+
457+
let AnonymousViewOp {
458+
id: ViewId(view_id),
459+
name: _,
460+
timestamp: _,
461+
args: view_args,
462+
} = op;
463+
// Serialize the arguments.
464+
let view_id = serialize_to_js(scope, &view_id)?;
465+
let view_args = serialize_to_js(scope, view_args.get_bsatn())?;
466+
let args = &[view_id, view_args];
467+
468+
// Call the function.
469+
let ret = call_free_fun(scope, fun, args)?;
470+
471+
// Deserialize the user result.
472+
let ret =
473+
cast!(scope, ret, v8::Uint8Array, "bytes return from `__call_view_anon__`").map_err(|e| e.throw(scope))?;
474+
let bytes = ret.get_contents(&mut []);
475+
476+
Ok(Bytes::copy_from_slice(bytes))
477+
}
478+
367479
/// Calls the registered `__describe_module__` function hook.
368480
pub(super) fn call_describe_module(
369481
scope: &mut PinScope<'_, '_>,

0 commit comments

Comments
 (0)