Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ slab = "0.4.7"
sled = "0.34.7"
smallvec = { version = "1.11", features = ["union", "const_generics"] }
socket2 = "0.5"
sourcemap = "9"
sqllogictest = "0.17"
sqllogictest-engines = "0.17"
sqlparser = "0.38.0"
Expand Down
6 changes: 6 additions & 0 deletions crates/bindings-typescript/src/lib/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ export function pushReducer(
lifecycle, // <- lifecycle flag lands here
});

// If the function isn't named (e.g. `function foobar() {}`), give it the same
// name as the reducer so that it's clear what it is in in backtraces.
if (!fn.name) {
Object.defineProperty(fn, 'name', { value: name, writable: false });
}

REDUCERS.push(fn);
}

Expand Down
4 changes: 2 additions & 2 deletions crates/bindings-typescript/src/server/procedures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { MODULE_DEF, type UntypedSchemaDef } from '../lib/schema';
import { Timestamp } from '../lib/timestamp';
import { httpClient } from './http_internal';
import { makeReducerCtx, sys } from './runtime';
import { callUserFunction, makeReducerCtx, sys } from './runtime';

const { freeze } = Object;

Expand Down Expand Up @@ -71,7 +71,7 @@ export function callProcedure(
};
freeze(ctx);

const ret = fn(ctx, args);
const ret = callUserFunction(fn, ctx, args);
const retBuf = new BinaryWriter(returnTypeBaseSize);
AlgebraicType.serializeValue(retBuf, returnType, ret, MODULE_DEF.typespace);
return retBuf.getBuffer();
Expand Down
21 changes: 17 additions & 4 deletions crates/bindings-typescript/src/server/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
type RangedIndex,
type UniqueIndex,
} from '../lib/indexes';
import { callProcedure } from './procedures';
import { callProcedure as callProcedure } from './procedures';
import {
REDUCERS,
type AuthCtx,
Expand Down Expand Up @@ -190,6 +190,19 @@ export const makeReducerCtx = (
senderAuth: AuthCtxImpl.fromSystemTables(connectionId, sender),
});

/**
* Call into a user function `fn` - the backtrace from an exception thrown in
* `fn` or one of its descendants in the callgraph will be stripped by host
* code in `crates/core/src/host/v8/error.rs` such that `fn` will be shown to
* be the root of the call stack.
*/
export const callUserFunction = function __spacetimedb_end_short_backtrace<
Args extends any[],
R,
>(fn: (...args: Args) => R, ...args: Args): R {
return fn(...args);
};

export const hooks: ModuleHooks = {
__describe_module__() {
const writer = new BinaryWriter(128);
Expand Down Expand Up @@ -218,7 +231,7 @@ export const hooks: ModuleHooks = {
)
);
try {
return REDUCERS[reducerId](ctx, args) ?? { tag: 'ok' };
return callUserFunction(REDUCERS[reducerId], ctx, args) ?? { tag: 'ok' };
} catch (e) {
if (e instanceof SenderError) {
return { tag: 'err', value: e.message };
Expand All @@ -243,7 +256,7 @@ export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = {
params,
MODULE_DEF.typespace
);
const ret = fn(ctx, args);
const ret = callUserFunction(fn, ctx, args);
const retBuf = new BinaryWriter(returnTypeBaseSize);
AlgebraicType.serializeValue(retBuf, returnType, ret, MODULE_DEF.typespace);
return retBuf.getBuffer();
Expand All @@ -261,7 +274,7 @@ export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = {
params,
MODULE_DEF.typespace
);
const ret = fn(ctx, args);
const ret = callUserFunction(fn, ctx, args);
const retBuf = new BinaryWriter(returnTypeBaseSize);
AlgebraicType.serializeValue(retBuf, returnType, ret, MODULE_DEF.typespace);
return retBuf.getBuffer();
Expand Down
27 changes: 18 additions & 9 deletions crates/cli/src/tasks/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use rolldown_utils::js_regex::HybridRegex;
use rolldown_utils::pattern_filter::StringOrRegex;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::sync::{Arc, OnceLock};
use tokio::runtime::{Builder, Handle, Runtime};

static RUNTIME: OnceLock<Runtime> = OnceLock::new();
Expand Down Expand Up @@ -62,14 +62,23 @@ pub(crate) fn build_javascript(project_path: &Path, build_debug: bool) -> anyhow
generated_code: Some(rolldown::GeneratedCodeOptions::es2015()),
es_module: Some(rolldown::EsModuleFlag::IfDefaultProp), // See https://rollupjs.org/configuration-options/#output-esmodule
drop_labels: None,
hash_characters: None, // File name hash characters, we don't care
banner: None, // String to prepend to the bundle
footer: None, // String to append to the bundle
intro: None, // Similar to the above, but inside the wrappers
outro: None, // Similar to the above, but inside the wrappers
sourcemap_base_url: None, // Absolute URLs for the source map
sourcemap_ignore_list: None, // See https://rollupjs.org/configuration-options/#output-sourcemapignorelist
sourcemap_path_transform: None, // Function to transform source map paths
hash_characters: None, // File name hash characters, we don't care
banner: None, // String to prepend to the bundle
footer: None, // String to append to the bundle
intro: None, // Similar to the above, but inside the wrappers
outro: None, // Similar to the above, but inside the wrappers
sourcemap_base_url: None, // Absolute URLs for the source map
sourcemap_ignore_list: None, // See https://rollupjs.org/configuration-options/#output-sourcemapignorelist
// Function to transform source map paths
sourcemap_path_transform: Some(rolldown::SourceMapPathTransform::new(Arc::new(
|relative_path, _sourcemap_path| {
// The output file is ./dist/bundle.js, so all the paths will be relative to it,
// e.g. from the perspective of `./dist` the entry file is `../src/index.ts`.
// So, strip the leading `../`
let path = relative_path.strip_prefix("../").unwrap_or(relative_path);
Box::pin(futures::future::ok(path.to_owned()))
},
))),
sourcemap_debug_ids: Some(true), // Seems like a good idea. See: https://rollupjs.org/configuration-options/#output-sourcemapdebugids
module_types: None, // Lets you associate file extensions with module types, e.g. `.data` -> `json`. We don't need this.
// Wrapper around https://docs.rs/oxc_resolver/latest/oxc_resolver/struct.ResolveOptions.html, see also https://rolldown.rs/guide/features#module-resolution
Expand Down
1 change: 1 addition & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ similar.workspace = true
slab.workspace = true
sled.workspace = true
smallvec.workspace = true
sourcemap.workspace = true
sqlparser.workspace = true
strum.workspace = true
tabled.workspace = true
Expand Down
80 changes: 73 additions & 7 deletions crates/core/src/host/v8/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{
replica_context::ReplicaContext,
};
use core::fmt;
use spacetimedb_data_structures::map::IntMap;
use spacetimedb_sats::Serialize;
use v8::{tc_scope, Exception, HandleScope, Local, PinScope, PinnedRef, StackFrame, StackTrace, TryCatch, Value};

Expand Down Expand Up @@ -266,6 +267,10 @@ impl JsStackTrace {
let frame = trace.get_frame(scope, index).unwrap();
JsStackTraceFrame::from_frame(scope, frame)
})
// A call frame with this name is the dividing line between user stack frames
// and module-bindings frames (e.g. `__call_reducer__`). See `callUserFunction`
// in `src/server/runtime.ts` in the Typescript SDK.
.take_while(|frame| frame.fn_name() != "__spacetimedb_end_short_backtrace")
.collect::<Box<[_]>>();
Self { frames }
}
Expand Down Expand Up @@ -338,16 +343,46 @@ pub(super) struct JsStackTraceFrame {
impl JsStackTraceFrame {
/// Converts a V8 [`StackFrame`] into one independent of `'scope`.
fn from_frame<'scope>(scope: &PinScope<'scope, '_>, frame: Local<'scope, StackFrame>) -> Self {
let script_name = frame
.get_script_name_or_source_url(scope)
.map(|s| s.to_rust_string_lossy(scope));

let script_id = frame.get_script_id();
let mut line = frame.get_line_number();
let mut column = frame.get_column();
let mut script_name = None;
let fn_name = frame.get_function_name(scope).map(|s| s.to_rust_string_lossy(scope));

let sourcemap = scope
.get_slot()
.and_then(|SourceMaps(maps)| maps.get(&(script_id as i32)));

// sourcemap uses 0-based line/column numbers, while v8 uses 1-based
if let Some(token) = sourcemap.and_then(|sm| sm.lookup_token(line as u32 - 1, column as u32 - 1)) {
line = token.get_src_line() as usize + 1;
column = token.get_src_col() as usize + 1;
if let Some(file) = token.get_source() {
script_name = Some(file.to_owned())
}

// If we ever want to support de-minifying function names, uncomment this.
// The process of obtaining the original name of a function given a token
// in that function is imperfect and could return an incorrect name for an
// unminified identifier. So until we need it, turn it off.
//
// if let Some((sv, fn_name)) = Option::zip(token.get_source_view(), fn_name.as_mut()) {
// if let Some(new_name) = sv.get_original_function_name(token, fn_name) {
// new_name.clone_into(fn_name)
// }
// }
}

let script_name = script_name.or_else(|| {
frame
.get_script_name_or_source_url(scope)
.map(|s| s.to_rust_string_lossy(scope))
});

Self {
line: frame.get_line_number(),
column: frame.get_column(),
script_id: frame.get_script_id(),
line,
column,
script_id,
script_name,
fn_name,
is_eval: frame.is_eval(),
Expand Down Expand Up @@ -401,6 +436,37 @@ impl fmt::Display for JsStackTraceFrame {
}
}

/// Mappings from a script id to its source map.
#[derive(Default)]
pub(super) struct SourceMaps(IntMap<i32, sourcemap::SourceMap>);

pub(super) fn parse_and_insert_sourcemap(scope: &mut PinScope<'_, '_>, module: Local<'_, v8::Module>) {
let source_map_url = module.get_unbound_module_script(scope).get_source_mapping_url(scope);
let source_map_url = (!source_map_url.is_null_or_undefined()).then_some(source_map_url);

if let Some((script_id, source_map_url)) = Option::zip(module.script_id(), source_map_url) {
let mut source_map_url = source_map_url.to_rust_string_lossy(scope);
// Hacky workaround for `decode_data_url` expecting a specific string without `charset=utf-8`
// Can remove once <https://github.com/getsentry/rust-sourcemap/pull/137> gets into a release
if source_map_url.starts_with("data:application/json;charset=utf-8;base64,") {
let start = "data:application/json;".len();
let len = "charset=utf-8;".len();
source_map_url.replace_range(start..start + len, "");
}
if let Ok(sourcemap::DecodedMap::Regular(sourcemap)) = sourcemap::decode_data_url(&source_map_url) {
let SourceMaps(maps) = get_or_insert_slot(scope, SourceMaps::default);
maps.insert(script_id, sourcemap);
}
}
}

fn get_or_insert_slot<T: 'static>(isolate: &mut v8::Isolate, default: impl FnOnce() -> T) -> &mut T {
if isolate.get_slot::<T>().is_none() {
isolate.set_slot(default());
}
isolate.get_slot_mut().unwrap()
}

impl JsError {
/// Turns a caught JS exception in `scope` into a [`JSError`].
fn from_caught(scope: &PinnedRef<'_, TryCatch<'_, '_, HandleScope<'_>>>) -> Self {
Expand Down
2 changes: 2 additions & 0 deletions crates/core/src/host/v8/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,8 @@ fn eval_module<'scope>(
return Err(error::TypeError("module has top-level await and is pending").throw(scope));
}

error::parse_and_insert_sourcemap(scope, module);

Ok((module, value))
}

Expand Down
Loading