Skip to content

Commit ffd7bc9

Browse files
committed
Basic procedure calling
1 parent 9e3ffeb commit ffd7bc9

File tree

10 files changed

+311
-51
lines changed

10 files changed

+311
-51
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { AlgebraicType, ProductType } from '../lib/algebraic_type';
2+
import BinaryReader from '../lib/binary_reader';
3+
import BinaryWriter from '../lib/binary_writer';
4+
import type { ConnectionId } from '../lib/connection_id';
5+
import type { Identity } from '../lib/identity';
6+
import type { Timestamp } from '../lib/timestamp';
7+
import type { ParamsObj } from './reducers';
8+
import { MODULE_DEF, type UntypedSchemaDef } from './schema';
9+
import type { Infer, InferTypeOfRow, TypeBuilder } from './type_builders';
10+
import { bsatnBaseSize } from './util';
11+
12+
export type ProcedureFn<
13+
S extends UntypedSchemaDef,
14+
Params extends ParamsObj,
15+
Ret extends TypeBuilder<any, any>,
16+
> = (ctx: ProcedureCtx<S>, args: InferTypeOfRow<Params>) => Infer<Ret>;
17+
18+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
19+
export type ProcedureCtx<S extends UntypedSchemaDef> = Readonly<{
20+
sender: Identity;
21+
timestamp: Timestamp;
22+
connectionId: ConnectionId | null;
23+
}>;
24+
25+
export function procedure<
26+
S extends UntypedSchemaDef,
27+
Params extends ParamsObj,
28+
Ret extends TypeBuilder<any, any>,
29+
>(name: string, params: Params, ret: Ret, fn: ProcedureFn<S, Params, Ret>) {
30+
const paramsType: ProductType = {
31+
elements: Object.entries(params).map(([n, c]) => ({
32+
name: n,
33+
algebraicType:
34+
'typeBuilder' in c ? c.typeBuilder.algebraicType : c.algebraicType,
35+
})),
36+
};
37+
const returnType = ret.algebraicType;
38+
39+
MODULE_DEF.miscExports.push({
40+
tag: 'Procedure',
41+
value: {
42+
name,
43+
params: paramsType,
44+
returnType,
45+
},
46+
});
47+
48+
PROCEDURES.push({
49+
fn,
50+
paramsType,
51+
returnType,
52+
returnTypeBaseSize: bsatnBaseSize(MODULE_DEF.typespace, returnType),
53+
});
54+
}
55+
56+
const PROCEDURES: Array<{
57+
fn: ProcedureFn<any, any, any>;
58+
paramsType: ProductType;
59+
returnType: AlgebraicType;
60+
returnTypeBaseSize: number;
61+
}> = [];
62+
63+
export function callProcedure(
64+
id: number,
65+
sender: Identity,
66+
connectionId: ConnectionId | null,
67+
timestamp: Timestamp,
68+
argsBuf: Uint8Array
69+
): Uint8Array {
70+
const { fn, paramsType, returnType, returnTypeBaseSize } = PROCEDURES[id];
71+
const args = ProductType.deserializeValue(
72+
new BinaryReader(argsBuf),
73+
paramsType,
74+
MODULE_DEF.typespace
75+
);
76+
const ret = fn(Object.freeze({ sender, connectionId, timestamp }), args);
77+
const retBuf = new BinaryWriter(returnTypeBaseSize);
78+
AlgebraicType.serializeValue(retBuf, returnType, ret, MODULE_DEF.typespace);
79+
return retBuf.getBuffer();
80+
}

crates/bindings-typescript/src/lib/schema.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
} from './views';
4848
import RawIndexDefV9 from './autogen/raw_index_def_v_9_type';
4949
import type { IndexOpts } from './indexes';
50+
import { procedure, type ProcedureFn } from './procedures';
5051

5152
export type TableNamesOf<S extends UntypedSchemaDef> =
5253
S['tables'][number]['name'];
@@ -547,6 +548,32 @@ class Schema<S extends UntypedSchemaDef> {
547548
// }
548549
// }
549550

551+
procedure<Params extends ParamsObj, Ret extends TypeBuilder<any, any>>(
552+
name: string,
553+
params: Params,
554+
ret: Ret,
555+
fn: ProcedureFn<S, Params, Ret>
556+
): ProcedureFn<S, Params, Ret>;
557+
procedure<Ret extends TypeBuilder<any, any>>(
558+
name: string,
559+
ret: Ret,
560+
fn: ProcedureFn<S, {}, Ret>
561+
): ProcedureFn<S, {}, Ret>;
562+
procedure<Params extends ParamsObj, Ret extends TypeBuilder<any, any>>(
563+
name: string,
564+
paramsOrRet: Ret | Params,
565+
retOrFn: ProcedureFn<S, {}, Ret> | Ret,
566+
maybeFn?: ProcedureFn<S, Params, Ret>
567+
): ProcedureFn<S, Params, Ret> {
568+
if (typeof retOrFn === 'function') {
569+
procedure(name, {}, paramsOrRet as Ret, retOrFn);
570+
return retOrFn;
571+
} else {
572+
procedure(name, paramsOrRet as Params, retOrFn, maybeFn!);
573+
return maybeFn!;
574+
}
575+
}
576+
550577
clientVisibilityFilter = {
551578
sql(filter: string): void {
552579
MODULE_DEF.rowLevelSecurity.push({ sql: filter });

crates/bindings-typescript/src/server/runtime.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AlgebraicType } from '../lib/algebraic_type';
1+
import { AlgebraicType, ProductType } from '../lib/algebraic_type';
22
import RawModuleDef from '../lib/autogen/raw_module_def_type';
33
import type RawModuleDefV9 from '../lib/autogen/raw_module_def_v_9_type';
44
import type RawTableDefV9 from '../lib/autogen/raw_table_def_v_9_type';
@@ -38,6 +38,7 @@ import {
3838
type AnonymousViewCtx,
3939
type ViewCtx,
4040
} from '../lib/views';
41+
import { callProcedure } from '../lib/procedures';
4142

4243
const { freeze } = Object;
4344

@@ -235,9 +236,9 @@ export const hooks_v1_1: import('spacetime:[email protected]').ModuleHooks = {
235236
// at runtime
236237
db: getDbView(),
237238
});
238-
const args = AlgebraicType.deserializeValue(
239+
const args = ProductType.deserializeValue(
239240
new BinaryReader(argsBuf),
240-
AlgebraicType.Product(params),
241+
params,
241242
MODULE_DEF.typespace
242243
);
243244
const ret = fn(ctx, args);
@@ -253,9 +254,9 @@ export const hooks_v1_1: import('spacetime:[email protected]').ModuleHooks = {
253254
// at runtime
254255
db: getDbView(),
255256
});
256-
const args = AlgebraicType.deserializeValue(
257+
const args = ProductType.deserializeValue(
257258
new BinaryReader(argsBuf),
258-
AlgebraicType.Product(params),
259+
params,
259260
MODULE_DEF.typespace
260261
);
261262
const ret = fn(ctx, args);
@@ -265,6 +266,18 @@ export const hooks_v1_1: import('spacetime:[email protected]').ModuleHooks = {
265266
},
266267
};
267268

269+
export const hooks_v1_2: import('spacetime:[email protected]').ModuleHooks = {
270+
__call_procedure__(id, sender, connection_id, timestamp, args) {
271+
return callProcedure(
272+
id,
273+
new Identity(sender),
274+
ConnectionId.nullIfZero(new ConnectionId(connection_id)),
275+
new Timestamp(timestamp),
276+
args
277+
);
278+
},
279+
};
280+
268281
let DB_VIEW: DbView<any> | null = null;
269282
function getDbView() {
270283
DB_VIEW ??= makeDbView(MODULE_DEF);

crates/bindings-typescript/src/server/sys.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,17 @@ declare module 'spacetime:[email protected]' {
7575

7676
export function register_hooks(hooks: ModuleHooks);
7777
}
78+
79+
declare module 'spacetime:[email protected]' {
80+
export type ModuleHooks = {
81+
__call_procedure__(
82+
id: u32,
83+
sender: u256,
84+
connection_id: u128,
85+
timestamp: bigint,
86+
args: Uint8Array
87+
): Uint8Array;
88+
};
89+
90+
export function register_hooks(hooks: ModuleHooks);
91+
}

crates/core/src/host/module_host.rs

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -400,13 +400,6 @@ impl Instance {
400400
Instance::Js(inst) => inst.trapped(),
401401
}
402402
}
403-
404-
async fn call_procedure(&mut self, params: CallProcedureParams) -> Result<ProcedureCallResult, ProcedureCallError> {
405-
match self {
406-
Instance::Wasm(inst) => inst.call_procedure(params).await,
407-
Instance::Js(inst) => inst.call_procedure(params).await,
408-
}
409-
}
410403
}
411404

412405
/// Creates the table for `table_def` in `stdb`.
@@ -1595,18 +1588,20 @@ impl ModuleHost {
15951588
let args = args.into_tuple(procedure_seed).map_err(InvalidProcedureArguments)?;
15961589
let caller_connection_id = caller_connection_id.unwrap_or(ConnectionId::ZERO);
15971590

1598-
self.call_async_with_instance(&procedure_def.name, async move |mut inst| {
1599-
let res = inst
1600-
.call_procedure(CallProcedureParams {
1601-
timestamp: Timestamp::now(),
1602-
caller_identity,
1603-
caller_connection_id,
1604-
timer,
1605-
procedure_id,
1606-
args,
1607-
})
1608-
.await;
1609-
(res, inst)
1591+
let params = CallProcedureParams {
1592+
timestamp: Timestamp::now(),
1593+
caller_identity,
1594+
caller_connection_id,
1595+
timer,
1596+
procedure_id,
1597+
args,
1598+
};
1599+
self.call_async_with_instance(&procedure_def.name, async move |inst| match inst {
1600+
Instance::Wasm(mut inst) => (inst.call_procedure(params).await, Instance::Wasm(inst)),
1601+
Instance::Js(inst) => {
1602+
let (r, s) = inst.call_procedure(params).await;
1603+
(r, Instance::Js(s))
1604+
}
16101605
})
16111606
.await?
16121607
}

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

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use self::error::{
66
use self::ser::serialize_to_js;
77
use self::string::{str_from_ident, IntoJsString};
88
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,
9+
call_call_procedure, call_call_reducer, call_call_view, call_call_view_anon, call_describe_module, get_hooks,
10+
resolve_sys_module, FnRet, HookFunctions,
1111
};
1212
use super::module_common::{build_common_module_from_raw, run_describer, ModuleCommon};
1313
use super::module_host::{CallProcedureParams, CallReducerParams, Module, ModuleInfo, ModuleRuntime};
@@ -26,14 +26,17 @@ use crate::host::wasm_common::module_host_actor::{
2626
WasmInstance,
2727
};
2828
use crate::host::wasm_common::{RowIters, TimingSpanSet};
29-
use crate::host::{ModuleHost, ReducerCallError, ReducerCallResult, Scheduler};
29+
use crate::host::{
30+
ModuleHost, ProcedureCallError, ProcedureCallResult, ReducerCallError, ReducerCallResult, Scheduler,
31+
};
3032
use crate::module_host_context::{ModuleCreationContext, ModuleCreationContextLimited};
3133
use crate::replica_context::ReplicaContext;
3234
use crate::util::asyncify;
3335
use anyhow::Context as _;
3436
use core::any::type_name;
3537
use core::str;
3638
use enum_as_inner::EnumAsInner;
39+
use futures::FutureExt;
3740
use itertools::Either;
3841
use spacetimedb_auth::identity::ConnectionAuthCtx;
3942
use spacetimedb_client_api_messages::energy::FunctionBudget;
@@ -381,10 +384,16 @@ impl JsInstance {
381384
}
382385

383386
pub async fn call_procedure(
384-
&mut self,
385-
_params: CallProcedureParams,
386-
) -> Result<super::ProcedureCallResult, super::ProcedureCallError> {
387-
todo!("JS/TS module procedure support")
387+
self: Box<Self>,
388+
params: CallProcedureParams,
389+
) -> (Result<ProcedureCallResult, ProcedureCallError>, Box<Self>) {
390+
let (r, s) = self
391+
.send_recv(
392+
JsWorkerReply::into_call_procedure,
393+
JsWorkerRequest::CallProcedure { params },
394+
)
395+
.await;
396+
(*r, s)
388397
}
389398

390399
pub async fn call_view(self: Box<Self>, tx: MutTxId, params: CallViewParams) -> (ViewCallResult, Box<Self>) {
@@ -401,6 +410,7 @@ enum JsWorkerReply {
401410
UpdateDatabase(anyhow::Result<UpdateDatabaseResult>),
402411
CallReducer(ReducerCallResult),
403412
CallView(Box<ViewCallResult>),
413+
CallProcedure(Box<Result<ProcedureCallResult, ProcedureCallError>>),
404414
ClearAllClients(anyhow::Result<()>),
405415
CallIdentityConnected(Result<(), ClientConnectedError>),
406416
CallIdentityDisconnected(Result<(), ReducerCallError>),
@@ -426,6 +436,8 @@ enum JsWorkerRequest {
426436
},
427437
/// See [`JsInstance::call_view`].
428438
CallView { tx: MutTxId, params: CallViewParams },
439+
/// See [`JsInstance::call_procedure`].
440+
CallProcedure { params: CallProcedureParams },
429441
/// See [`JsInstance::clear_all_clients`].
430442
ClearAllClients,
431443
/// See [`JsInstance::call_identity_connected`].
@@ -579,6 +591,14 @@ fn spawn_instance_worker(
579591
let (res, trapped) = instance_common.call_view_with_tx(tx, params, &mut inst);
580592
reply("call_view", JsWorkerReply::CallView(res.into()), trapped);
581593
}
594+
JsWorkerRequest::CallProcedure { params } => {
595+
let (res, trapped) = instance_common
596+
.call_procedure(params, &mut inst)
597+
.now_or_never()
598+
.expect("our call_procedure implementation is not actually async");
599+
600+
reply("call_procedure", JsWorkerReply::CallProcedure(res.into()), trapped);
601+
}
582602
JsWorkerRequest::ClearAllClients => {
583603
let res = instance_common.clear_all_clients();
584604
reply("clear_all_clients", ClearAllClients(res), false);
@@ -758,11 +778,10 @@ impl WasmInstance for V8Instance<'_, '_, '_> {
758778
}
759779

760780
fn call_reducer(&mut self, op: ReducerOp<'_>, budget: FunctionBudget) -> ReducerExecuteResult {
761-
let ExecutionResult { stats, call_result } = common_call(self.scope, budget, op, |scope, op| {
781+
common_call(self.scope, budget, op, |scope, op| {
762782
Ok(call_call_reducer(scope, self.hooks, op)?)
763-
});
764-
let call_result = call_result.and_then(|res| res.map_err(ExecutionError::User));
765-
ExecutionResult { stats, call_result }
783+
})
784+
.map_result(|call_result| call_result.and_then(|res| res.map_err(ExecutionError::User)))
766785
}
767786

768787
fn call_view(&mut self, op: ViewOp<'_>, budget: FunctionBudget) -> ViewExecuteResult {
@@ -781,8 +800,16 @@ impl WasmInstance for V8Instance<'_, '_, '_> {
781800
log_traceback(self.replica_ctx, func_type, func, trap)
782801
}
783802

784-
async fn call_procedure(&mut self, _op: ProcedureOp, _budget: FunctionBudget) -> ProcedureExecuteResult {
785-
todo!("JS/TS module procedure support")
803+
async fn call_procedure(&mut self, op: ProcedureOp, budget: FunctionBudget) -> ProcedureExecuteResult {
804+
common_call(self.scope, budget, op, |scope, op| {
805+
call_call_procedure(scope, self.hooks, op)
806+
})
807+
.map_result(|call_result| {
808+
call_result.map_err(|e| match e {
809+
ExecutionError::User(e) => anyhow::Error::msg(e),
810+
ExecutionError::Recoverable(e) | ExecutionError::Trap(e) => e,
811+
})
812+
})
786813
}
787814
}
788815

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub(in super::super) enum ModuleHookKey {
5050
CallReducer,
5151
CallView,
5252
CallAnonymousView,
53+
CallProcedure,
5354
}
5455

5556
impl ModuleHookKey {
@@ -63,6 +64,7 @@ impl ModuleHookKey {
6364
ModuleHookKey::CallReducer => 21,
6465
ModuleHookKey::CallView => 22,
6566
ModuleHookKey::CallAnonymousView => 23,
67+
ModuleHookKey::CallProcedure => 24,
6668
}
6769
}
6870
}
@@ -110,6 +112,7 @@ pub(in super::super) struct HookFunctions<'scope> {
110112
pub call_reducer: Local<'scope, Function>,
111113
pub call_view: Option<Local<'scope, Function>>,
112114
pub call_view_anon: Option<Local<'scope, Function>>,
115+
pub call_procedure: Option<Local<'scope, Function>>,
113116
}
114117

115118
/// Returns the hook function previously registered in [`register_hooks`].
@@ -131,5 +134,6 @@ pub(in super::super) fn get_hooks<'scope>(scope: &mut PinScope<'scope, '_>) -> O
131134
call_reducer: get(ModuleHookKey::CallReducer)?,
132135
call_view: get(ModuleHookKey::CallView),
133136
call_view_anon: get(ModuleHookKey::CallAnonymousView),
137+
call_procedure: get(ModuleHookKey::CallProcedure),
134138
})
135139
}

0 commit comments

Comments
 (0)