diff --git a/crates/bindings-typescript/src/lib/binary_reader.ts b/crates/bindings-typescript/src/lib/binary_reader.ts index 1b7050ac8bb..83767755287 100644 --- a/crates/bindings-typescript/src/lib/binary_reader.ts +++ b/crates/bindings-typescript/src/lib/binary_reader.ts @@ -17,8 +17,11 @@ export default class BinaryReader { */ offset: number = 0; - constructor(input: Uint8Array) { - this.view = new DataView(input.buffer, input.byteOffset, input.byteLength); + constructor(input: Uint8Array | DataView) { + this.view = + input instanceof DataView + ? input + : new DataView(input.buffer, input.byteOffset, input.byteLength); this.offset = 0; } diff --git a/crates/bindings-typescript/src/lib/binary_writer.ts b/crates/bindings-typescript/src/lib/binary_writer.ts index 4d5f0eb79d7..d88aa2faf59 100644 --- a/crates/bindings-typescript/src/lib/binary_writer.ts +++ b/crates/bindings-typescript/src/lib/binary_writer.ts @@ -14,23 +14,59 @@ const ArrayBufferPrototypeTransfer = } }; +export class ResizableBuffer { + #buffer: ArrayBuffer | null; + + constructor(init: number | ArrayBuffer) { + this.#buffer = typeof init === 'number' ? new ArrayBuffer(init) : init; + } + + get buffer(): ArrayBuffer { + if (this.#buffer == null) throw new TypeError('Accessing detached buffer'); + return this.#buffer; + } + + get capacity(): number { + return this.buffer.byteLength; + } + + grow(newSize: number) { + if (this.#buffer == null) + throw new TypeError('Cannot resize detached buffer'); + if (newSize <= this.#buffer.byteLength) return; + this.#buffer = ArrayBufferPrototypeTransfer.call(this.#buffer, newSize); + } + + get detached(): boolean { + return this.#buffer == null; + } + + detach(): ArrayBuffer { + if (this.#buffer == null) + throw new TypeError('Cannot detach detached buffer'); + const buf = this.#buffer!; + this.#buffer = null; + return buf; + } +} + export default class BinaryWriter { - #buffer: ArrayBuffer; + #buffer: ResizableBuffer; view: DataView; offset: number = 0; - constructor(size: number) { - this.#buffer = new ArrayBuffer(size); - this.view = new DataView(this.#buffer); + constructor(init: number | ResizableBuffer) { + this.#buffer = typeof init === 'number' ? new ResizableBuffer(init) : init; + this.view = new DataView(this.#buffer.buffer); } expandBuffer(additionalCapacity: number): void { const minCapacity = this.offset + additionalCapacity + 1; - if (minCapacity <= this.#buffer.byteLength) return; - let newCapacity = this.#buffer.byteLength * 2; + if (minCapacity <= this.#buffer.capacity) return; + let newCapacity = this.#buffer.capacity * 2; if (newCapacity < minCapacity) newCapacity = minCapacity; - this.#buffer = ArrayBufferPrototypeTransfer.call(this.#buffer, newCapacity); - this.view = new DataView(this.#buffer); + this.#buffer.grow(newCapacity); + this.view = new DataView(this.#buffer.buffer); } toBase64(): string { @@ -38,7 +74,7 @@ export default class BinaryWriter { } getBuffer(): Uint8Array { - return new Uint8Array(this.#buffer, 0, this.offset); + return new Uint8Array(this.#buffer.buffer, 0, this.offset); } writeUInt8Array(value: Uint8Array): void { @@ -47,7 +83,7 @@ export default class BinaryWriter { this.expandBuffer(4 + length); this.writeU32(length); - new Uint8Array(this.#buffer, this.offset).set(value); + new Uint8Array(this.#buffer.buffer, this.offset).set(value); this.offset += length; } diff --git a/crates/bindings-typescript/src/server/register_hooks.ts b/crates/bindings-typescript/src/server/register_hooks.ts index 5861f347e78..7a6a64edd8b 100644 --- a/crates/bindings-typescript/src/server/register_hooks.ts +++ b/crates/bindings-typescript/src/server/register_hooks.ts @@ -1,8 +1,4 @@ -import { register_hooks } from 'spacetime:sys@1.0'; -import { register_hooks as register_hooks_v1_1 } from 'spacetime:sys@1.1'; -import { register_hooks as register_hooks_v1_2 } from 'spacetime:sys@1.2'; -import { hooks, hooks_v1_1, hooks_v1_2 } from './runtime'; +import { register_hooks } from 'spacetime:sys@2.0'; +import { hooks } from './runtime'; register_hooks(hooks); -register_hooks_v1_1(hooks_v1_1); -register_hooks_v1_2(hooks_v1_2); diff --git a/crates/bindings-typescript/src/server/runtime.ts b/crates/bindings-typescript/src/server/runtime.ts index 8942cd221fa..b6d11cfc49d 100644 --- a/crates/bindings-typescript/src/server/runtime.ts +++ b/crates/bindings-typescript/src/server/runtime.ts @@ -1,8 +1,6 @@ -import * as _syscalls1_0 from 'spacetime:sys@1.0'; -import * as _syscalls1_2 from 'spacetime:sys@1.2'; -import * as _syscalls1_3 from 'spacetime:sys@1.3'; +import * as _syscalls2_0 from 'spacetime:sys@2.0'; -import type { ModuleHooks, u16, u32 } from 'spacetime:sys@1.0'; +import type { ModuleHooks, u16, u32 } from 'spacetime:sys@2.0'; import { AlgebraicType, ProductType, @@ -17,7 +15,7 @@ import { Identity } from '../lib/identity'; import { Timestamp } from '../lib/timestamp'; import { Uuid } from '../lib/uuid'; import BinaryReader from '../lib/binary_reader'; -import BinaryWriter from '../lib/binary_writer'; +import BinaryWriter, { ResizableBuffer } from '../lib/binary_writer'; import { type Index, type IndexVal, @@ -35,7 +33,7 @@ import { import { type UntypedSchemaDef } from '../lib/schema'; import { type RowType, type Table, type TableMethods } from '../lib/table'; import type { Infer } from '../lib/type_builders'; -import { bsatnBaseSize, hasOwn, toCamelCase } from '../lib/util'; +import { hasOwn, toCamelCase } from '../lib/util'; import { type AnonymousViewCtx, type ViewCtx } from './views'; import { isRowTypedQuery, makeQueryBuilder, toSql } from './query'; import type { DbView } from './db_view'; @@ -47,9 +45,7 @@ import { getRegisteredSchema } from './schema'; const { freeze } = Object; -export const sys = freeze( - wrapSyscalls(_syscalls1_0, _syscalls1_2, _syscalls1_3) -); +export const sys = freeze(wrapSyscalls(_syscalls2_0)); export function parseJsonObject(json: string): JsonObject { let value: unknown; @@ -297,9 +293,6 @@ export const hooks: ModuleHooks = { throw e; } }, -}; - -export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = { __call_view__(id, sender, argsBuf) { const moduleCtx = getRegisteredSchema(); const { fn, deserializeParams, serializeReturn, returnTypeBaseSize } = @@ -347,9 +340,6 @@ export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = { } return { data: retBuf.getBuffer() }; }, -}; - -export const hooks_v1_2: import('spacetime:sys@1.2').ModuleHooks = { __call_procedure__(id, sender, connection_id, timestamp, args) { return callProcedure( getRegisteredSchema(), @@ -392,8 +382,6 @@ function makeTableView( const serializeRow = AlgebraicType.makeSerializer(rowType, typespace); const deserializeRow = AlgebraicType.makeDeserializer(rowType, typespace); - const baseSize = bsatnBaseSize(typespace, rowType); - const sequences = table.sequences.map(seq => { const col = rowType.value.elements[seq.column]; const colType = col.algebraicType; @@ -450,21 +438,24 @@ function makeTableView( iter, [Symbol.iterator]: () => iter(), insert: row => { - const writer = new BinaryWriter(baseSize); + const buf = LEAF_BUF; + const writer = new BinaryWriter(buf); serializeRow(writer, row); - const ret_buf = sys.datastore_insert_bsatn(table_id, writer.getBuffer()); + sys.datastore_insert_bsatn(table_id, buf.buffer, writer.offset); const ret = { ...row }; - integrateGeneratedColumns?.(ret, ret_buf); + integrateGeneratedColumns?.(ret, new Uint8Array(buf.buffer)); return ret; }, delete: (row: RowType): boolean => { - const writer = new BinaryWriter(4 + baseSize); + const buf = LEAF_BUF; + const writer = new BinaryWriter(buf); writer.writeU32(1); serializeRow(writer, row); const count = sys.datastore_delete_all_by_eq_bsatn( table_id, - writer.getBuffer() + buf.buffer, + writer.offset ); return count > 0; }, @@ -503,12 +494,12 @@ function makeTableView( ) ); - const serializePoint = (colVal: any[]): Uint8Array => { - const writer = new BinaryWriter(baseSize); + const serializePoint = (buffer: ResizableBuffer, colVal: any[]): number => { + const writer = new BinaryWriter(buffer); for (let i = 0; i < numColumns; i++) { indexSerializers[i](writer, colVal[i]); } - return writer.getBuffer(); + return writer.offset; }; const serializeSingleElement = @@ -516,17 +507,17 @@ function makeTableView( const serializeSinglePoint = serializeSingleElement && - ((colVal: any): Uint8Array => { - const writer = new BinaryWriter(baseSize); + ((buffer: ResizableBuffer, colVal: any): number => { + const writer = new BinaryWriter(buffer); serializeSingleElement(writer, colVal); - return writer.getBuffer(); + return writer.offset; }); type IndexScanArgs = [ - prefix: Uint8Array, + prefix_len: u32, prefix_elems: u16, - rstart: Uint8Array, - rend: Uint8Array, + rstart_len: u32, + rend_len: u32, ]; let index: Index; @@ -534,11 +525,14 @@ function makeTableView( // numColumns == 1, unique index index = { find: (colVal: IndexVal): RowType | null => { - const point = serializeSinglePoint(colVal); - const iter = tableIterator( - sys.datastore_index_scan_point_bsatn(index_id, point), - deserializeRow + const buf = LEAF_BUF; + const point_len = serializeSinglePoint(buf, colVal); + const iter_id = sys.datastore_index_scan_point_bsatn( + index_id, + buf.buffer, + point_len ); + const iter = tableIterator(iter_id, deserializeRow); const { value, done } = iter.next(); if (done) return null; if (!iter.next().done) @@ -548,22 +542,26 @@ function makeTableView( return value; }, delete: (colVal: IndexVal): boolean => { - const point = serializeSinglePoint(colVal); + const buf = LEAF_BUF; + const point_len = serializeSinglePoint(buf, colVal); const num = sys.datastore_delete_by_index_scan_point_bsatn( index_id, - point + buf.buffer, + point_len ); return num > 0; }, update: (row: RowType): RowType => { - const writer = new BinaryWriter(baseSize); + const buf = LEAF_BUF; + const writer = new BinaryWriter(buf); serializeRow(writer, row); - const ret_buf = sys.datastore_update_bsatn( + sys.datastore_update_bsatn( table_id, index_id, - writer.getBuffer() + buf.buffer, + writer.offset ); - integrateGeneratedColumns?.(row, ret_buf); + integrateGeneratedColumns?.(row, new Uint8Array(buf.buffer)); return row; }, } as UniqueIndex; @@ -574,11 +572,14 @@ function makeTableView( if (colVal.length !== numColumns) { throw new TypeError('wrong number of elements'); } - const point = serializePoint(colVal); - const iter = tableIterator( - sys.datastore_index_scan_point_bsatn(index_id, point), - deserializeRow + const buf = LEAF_BUF; + const point_len = serializePoint(buf, colVal); + const iter_id = sys.datastore_index_scan_point_bsatn( + index_id, + buf.buffer, + point_len ); + const iter = tableIterator(iter_id, deserializeRow); const { value, done } = iter.next(); if (done) return null; if (!iter.next().done) @@ -591,22 +592,26 @@ function makeTableView( if (colVal.length !== numColumns) throw new TypeError('wrong number of elements'); - const point = serializePoint(colVal); + const buf = LEAF_BUF; + const point_len = serializePoint(buf, colVal); const num = sys.datastore_delete_by_index_scan_point_bsatn( index_id, - point + buf.buffer, + point_len ); return num > 0; }, update: (row: RowType): RowType => { - const writer = new BinaryWriter(baseSize); + const buf = LEAF_BUF; + const writer = new BinaryWriter(buf); serializeRow(writer, row); - const ret_buf = sys.datastore_update_bsatn( + sys.datastore_update_bsatn( table_id, index_id, - writer.getBuffer() + buf.buffer, + writer.offset ); - integrateGeneratedColumns?.(row, ret_buf); + integrateGeneratedColumns?.(row, new Uint8Array(buf.buffer)); return row; }, } as UniqueIndex; @@ -614,26 +619,34 @@ function makeTableView( // numColumns == 1 index = { filter: (range: any): IteratorObject> => { - const point = serializeSinglePoint(range); - return tableIterator( - sys.datastore_index_scan_point_bsatn(index_id, point), - deserializeRow + const buf = LEAF_BUF; + const point_len = serializeSinglePoint(buf, range); + const iter_id = sys.datastore_index_scan_point_bsatn( + index_id, + buf.buffer, + point_len ); + return tableIterator(iter_id, deserializeRow); }, delete: (range: any): u32 => { - const point = serializeSinglePoint(range); + const buf = LEAF_BUF; + const point_len = serializeSinglePoint(buf, range); return sys.datastore_delete_by_index_scan_point_bsatn( index_id, - point + buf.buffer, + point_len ); }, } as RangedIndex; } else { // numColumns != 1 - const serializeRange = (range: any[]): IndexScanArgs => { + const serializeRange = ( + buffer: ResizableBuffer, + range: any[] + ): IndexScanArgs => { if (range.length > numColumns) throw new TypeError('too many elements'); - const writer = new BinaryWriter(baseSize + 1); + const writer = new BinaryWriter(buffer); const prefix_elems = range.length - 1; for (let i = 0; i < prefix_elems; i++) { indexSerializers[i](writer, range[i]); @@ -641,7 +654,6 @@ function makeTableView( const rstartOffset = writer.offset; const term = range[range.length - 1]; const serializeTerm = indexSerializers[range.length - 1]; - let rstart: Uint8Array, rend: Uint8Array; if (term instanceof Range) { const writeBound = (bound: Bound) => { const tags = { included: 0, excluded: 1, unbounded: 2 }; @@ -649,46 +661,55 @@ function makeTableView( if (bound.tag !== 'unbounded') serializeTerm(writer, bound.value); }; writeBound(term.from); - const rendOffset = writer.offset; + const rstartLen = writer.offset - rstartOffset; writeBound(term.to); - rstart = writer.getBuffer().slice(rstartOffset, rendOffset); - rend = writer.getBuffer().slice(rendOffset); + const rendLen = writer.offset - rstartLen; + return [rstartOffset, prefix_elems, rstartLen, rendLen]; } else { writer.writeU8(0); serializeTerm(writer, term); - rstart = rend = writer.getBuffer().slice(rstartOffset); + const rstartLen = writer.offset; + const rendLen = 0; + return [rstartOffset, prefix_elems, rstartLen, rendLen]; } - const buf = writer.getBuffer(); - const prefix = buf.slice(0, rstartOffset); - return [prefix, prefix_elems, rstart, rend]; }; index = { filter: (range: any[]): IteratorObject> => { if (range.length === numColumns) { - const point = serializePoint(range); - return tableIterator( - sys.datastore_index_scan_point_bsatn(index_id, point), - deserializeRow + const buf = LEAF_BUF; + const point_len = serializePoint(buf, range); + const iter_id = sys.datastore_index_scan_point_bsatn( + index_id, + buf.buffer, + point_len ); + return tableIterator(iter_id, deserializeRow); } else { - const args = serializeRange(range); - return tableIterator( - sys.datastore_index_scan_range_bsatn(index_id, ...args), - deserializeRow + const buf = LEAF_BUF; + const args = serializeRange(buf, range); + const iter_id = sys.datastore_index_scan_range_bsatn( + index_id, + buf.buffer, + ...args ); + return tableIterator(iter_id, deserializeRow); } }, delete: (range: any[]): u32 => { if (range.length === numColumns) { - const point = serializePoint(range); + const buf = LEAF_BUF; + const point_len = serializePoint(buf, range); return sys.datastore_delete_by_index_scan_point_bsatn( index_id, - point + buf.buffer, + point_len ); } else { - const args = serializeRange(range); + const buf = LEAF_BUF; + const args = serializeRange(buf, range); return sys.datastore_delete_by_index_scan_range_bsatn( index_id, + buf.buffer, ...args ); } @@ -712,23 +733,27 @@ function* tableIterator( ): Generator { using iter = new IteratorHandle(id); - let buf; - while ((buf = advanceIter(iter)) != null) { - const reader = new BinaryReader(buf); - while (reader.remaining > 0) { - yield deserialize(reader); + const iterBuf = takeBuf(); + try { + let amt; + while ((amt = advanceIter(iter, iterBuf))) { + const reader = new BinaryReader(new Uint8Array(iterBuf.buffer, amt)); + while (reader.offset < amt) { + yield deserialize(reader); + } } + } finally { + returnBuf(iterBuf); } } -function advanceIter(iter: IteratorHandle): Uint8Array | null { - let buf_max_len = 0x10000; +function advanceIter(iter: IteratorHandle, buf: ResizableBuffer): number { while (true) { try { - return iter.advance(buf_max_len); + return iter.advance(buf.buffer); } catch (e) { if (e && typeof e === 'object' && hasOwn(e, '__buffer_too_small__')) { - buf_max_len = e.__buffer_too_small__ as number; + buf.grow(e.__buffer_too_small__ as number); continue; } throw e; @@ -736,6 +761,32 @@ function advanceIter(iter: IteratorHandle): Uint8Array | null { } } +// This should guarantee in most cases that we don't have to reallocate an iterator +// buffer, unless there's a single row that serializes to >1 MiB. +const DEFAULT_BUFFER_CAPACITY = 32 * 1024 * 2; + +const ITER_BUFS: ResizableBuffer[] = [ + new ResizableBuffer(DEFAULT_BUFFER_CAPACITY), +]; +let ITER_BUF_COUNT = 1; + +function takeBuf(): ResizableBuffer { + return ITER_BUF_COUNT + ? ITER_BUFS[--ITER_BUF_COUNT] + : new ResizableBuffer(DEFAULT_BUFFER_CAPACITY); +} + +function returnBuf(buf: ResizableBuffer) { + ITER_BUFS[ITER_BUF_COUNT++] = buf; +} + +/** + * This should only be used from functions that don't need persistent ownership + * over the buffer. While using this value, one should not call a function that + * also uses this value. + */ +const LEAF_BUF = new ResizableBuffer(DEFAULT_BUFFER_CAPACITY); + /** A class to manage the lifecycle of an iterator handle. */ class IteratorHandle implements Disposable { #id: u32 | -1; @@ -757,15 +808,14 @@ class IteratorHandle implements Disposable { return id; } - /** Call `row_iter_bsatn_advance`, returning null if this iterator was already exhausted. */ - advance(buf_max_len: u32): Uint8Array | null { - if (this.#id === -1) return null; - const { 0: done, 1: buf } = sys.row_iter_bsatn_advance( - this.#id, - buf_max_len - ); - if (done) this.#detach(); - return buf; + /** Call `row_iter_bsatn_advance`, returning 0 if this iterator has been exhausted. */ + advance(buf: ArrayBuffer): number { + if (this.#id === -1) return 0; + // coerce to int32 + const ret = 0 | sys.row_iter_bsatn_advance(this.#id, buf); + // ret <= 0 means the iterator is exhausted + if (ret <= 0) this.#detach(); + return ret < 0 ? -ret : ret; } [Symbol.dispose]() { diff --git a/crates/bindings-typescript/src/server/sys.d.ts b/crates/bindings-typescript/src/server/sys.d.ts index f3480b5d186..bf2cf75ac1b 100644 --- a/crates/bindings-typescript/src/server/sys.d.ts +++ b/crates/bindings-typescript/src/server/sys.d.ts @@ -1,4 +1,4 @@ -declare module 'spacetime:sys@1.0' { +declare module 'spacetime:sys@2.0' { export type u8 = number; export type u16 = number; export type u32 = number; @@ -14,8 +14,20 @@ declare module 'spacetime:sys@1.0' { sender: u256, connId: u128, timestamp: bigint, - argsBuf: Uint8Array - ): { tag: 'ok' } | { tag: 'err'; value: string }; + argsBuf: DataView + ): undefined | { tag: 'ok' } | { tag: 'err'; value: string }; + + __call_view__(id: u32, sender: u256, args: Uint8Array): Uint8Array | object; + + __call_view_anon__(id: u32, args: Uint8Array): Uint8Array | object; + + __call_procedure__( + id: u32, + sender: u256, + connection_id: u128, + timestamp: bigint, + args: Uint8Array + ): Uint8Array; }; export function register_hooks(hooks: ModuleHooks); @@ -26,35 +38,37 @@ declare module 'spacetime:sys@1.0' { export function datastore_table_scan_bsatn(table_id: u32): u32; export function datastore_index_scan_range_bsatn( index_id: u32, - prefix: Uint8Array, + buf: ArrayBuffer, + prefix_len: u32, prefix_elems: u16, - rstart: Uint8Array, - rend: Uint8Array + rstart_len: u32, + rend_len: u32 ): u32; - export function row_iter_bsatn_advance( - iter: u32, - buffer_max_len: u32 - ): [boolean, Uint8Array]; + export function row_iter_bsatn_advance(iter: u32, buffer: ArrayBuffer): u32; export function row_iter_bsatn_close(iter: u32): void; export function datastore_insert_bsatn( table_id: u32, - row: Uint8Array - ): Uint8Array; + row: ArrayBuffer, + row_len: u32 + ): u32; export function datastore_update_bsatn( table_id: u32, index_id: u32, - row: Uint8Array - ): Uint8Array; + row: ArrayBuffer, + row_len: u32 + ): u32; export function datastore_delete_by_index_scan_range_bsatn( index_id: u32, - prefix: Uint8Array, + buf: ArrayBuffer, + prefix_len: u32, prefix_elems: u16, - rstart: Uint8Array, - rend: Uint8Array + rstart_len: u32, + rend_len: u32 ): u32; export function datastore_delete_all_by_eq_bsatn( table_id: u32, - relation: Uint8Array + relation: ArrayBuffer, + relation_len: u32 ): u32; export function volatile_nonatomic_schedule_immediate( reducer_name: string, @@ -65,29 +79,6 @@ declare module 'spacetime:sys@1.0' { export function console_timer_end(span_id: u32): void; export function identity(): { __identity__: u256 }; export function get_jwt_payload(connection_id: u128): Uint8Array; -} - -declare module 'spacetime:sys@1.1' { - export type ModuleHooks = { - __call_view__(id: u32, sender: u256, args: Uint8Array): Uint8Array | object; - __call_view_anon__(id: u32, args: Uint8Array): Uint8Array | object; - }; - - export function register_hooks(hooks: ModuleHooks); -} - -declare module 'spacetime:sys@1.2' { - export type ModuleHooks = { - __call_procedure__( - id: u32, - sender: u256, - connection_id: u128, - timestamp: bigint, - args: Uint8Array - ): Uint8Array; - }; - - export function register_hooks(hooks: ModuleHooks); export function procedure_http_request( request: Uint8Array, @@ -99,16 +90,16 @@ declare module 'spacetime:sys@1.2' { export function procedure_commit_mut_tx(); export function procedure_abort_mut_tx(); -} -declare module 'spacetime:sys@1.3' { export function datastore_index_scan_point_bsatn( index_id: u32, - point: Uint8Array + point: ArrayBuffer, + point_len: u32 ): u32; export function datastore_delete_by_index_scan_point_bsatn( index_id: u32, - point: Uint8Array + point: ArrayBuffer, + point_len: u32 ): u32; } diff --git a/crates/core/src/host/v8/error.rs b/crates/core/src/host/v8/error.rs index 55792c609bc..22ffc292ece 100644 --- a/crates/core/src/host/v8/error.rs +++ b/crates/core/src/host/v8/error.rs @@ -2,6 +2,9 @@ use super::serialize_to_js; use super::string::IntoJsString; +use crate::error::NodesError; +use crate::host::wasm_common::err_to_errno_and_log; +use crate::host::AbiCall; use crate::{ database_logger::{BacktraceFrame, BacktraceProvider, LogLevel, ModuleBacktrace, Record}, host::instance_env::InstanceEnv, @@ -9,6 +12,7 @@ use crate::{ }; use core::fmt; use spacetimedb_data_structures::map::IntMap; +use spacetimedb_primitives::errno; use spacetimedb_sats::Serialize; use v8::{tc_scope, Exception, HandleScope, Local, PinScope, PinnedRef, StackFrame, StackTrace, TryCatch, Value}; @@ -139,6 +143,26 @@ impl CodeError { } } +/// Throws `{ __code_error__: code }`. +pub(super) fn code_error(scope: &PinScope<'_, '_>, code: u16) -> ExceptionThrown { + let res = CodeError::from_code(scope, code); + collapse_exc_thrown(scope, res) +} + +/// Throws `{ __code_error__: NO_SUCH_ITER }`. +pub(super) fn no_such_iter(scope: &PinScope<'_, '_>) -> SysCallError { + code_error(scope, errno::NO_SUCH_ITER.get()).into() +} + +/// Collapses `res` where the `Ok(x)` where `x` is throwable. +pub(super) fn collapse_exc_thrown<'scope>( + scope: &PinScope<'scope, '_>, + res: ExcResult>, +) -> ExceptionThrown { + let (Ok(thrown) | Err(thrown)) = res.map(|ev| ev.throw(scope)); + thrown +} + /// A catchable error code thrown in callbacks /// to indicate bad arguments to a syscall. #[derive(Serialize)] @@ -162,6 +186,78 @@ impl CodeMessageError { } } +/// Either an exception, already thrown, or [`NodesError`] arising from [`InstanceEnv`]. +#[derive(derive_more::From)] +pub(super) enum SysCallError { + NoEnv, + /// Only occurs in the v2 ABI. + OutOfBounds, + Error(NodesError), + Exception(ExceptionThrown), +} + +/// Converts a `SysCallError` into a `ExceptionThrown`. +pub(super) fn handle_sys_call_error<'scope>( + abi_call: AbiCall, + scope: &mut PinScope<'scope, '_>, + err: SysCallError, +) -> ExceptionThrown { + const ENV_NOT_SET: u16 = 1; + match err { + SysCallError::NoEnv => code_error(scope, ENV_NOT_SET), + SysCallError::OutOfBounds => RangeError("length argument was out of bounds for `ArrayBuffer`").throw(scope), + SysCallError::Exception(exc) => exc, + SysCallError::Error(error) => throw_nodes_error(abi_call, scope, error), + } +} + +/// An out-of-bounds syscall error. +pub const OOB: SysCallError = SysCallError::OutOfBounds; + +/// A result where the error is a [`SysCallError`]. +pub type SysCallResult = Result; + +/// A flag set in [`throw_nodes_error`]. +/// The flag should be checked in every module -> host ABI. +/// If the flag is set, the call is prevented. +struct TerminationFlag; + +/// Checks the termination flag and throws a `TerminationError` if set. +/// +/// Returns whether the flag was set. +pub(super) fn throw_if_terminated(scope: &PinScope<'_, '_>) -> bool { + // If the flag was set in `throw_nodes_error`, + // we need to block all module -> host ABI calls. + let set = scope.get_slot::().is_some(); + if set { + let err = anyhow::anyhow!("execution is being terminated"); + if let Ok(exception) = TerminationError::from_error(scope, &err) { + exception.throw(scope); + } + } + + set +} + +/// Turns a [`NodesError`] into a thrown exception. +fn throw_nodes_error(abi_call: AbiCall, scope: &mut PinScope<'_, '_>, error: NodesError) -> ExceptionThrown { + let res = match err_to_errno_and_log::(abi_call, error) { + Ok((code, None)) => CodeError::from_code(scope, code), + Ok((code, Some(message))) => CodeMessageError::from_code(scope, code, message), + Err(err) => { + // Terminate execution ASAP and throw a catchable exception (`TerminationError`). + // Unfortunately, JS execution won't be terminated once the callback returns, + // so we set a slot that all callbacks immediately check + // to ensure that the module won't be able to do anything to the host + // while it's being terminated (eventually). + scope.terminate_execution(); + scope.set_slot(TerminationFlag); + TerminationError::from_error(scope, &err) + } + }; + collapse_exc_thrown(scope, res) +} + /// A catchable error code thrown in callbacks /// to indicate that a buffer was too small and the minimum size required. #[derive(Serialize)] diff --git a/crates/core/src/host/v8/mod.rs b/crates/core/src/host/v8/mod.rs index 2b76c391bc7..ba051806370 100644 --- a/crates/core/src/host/v8/mod.rs +++ b/crates/core/src/host/v8/mod.rs @@ -1,7 +1,7 @@ use self::budget::energy_from_elapsed; use self::error::{ - catch_exception, exception_already_thrown, log_traceback, BufferTooSmall, CanContinue, ErrorOrException, ExcResult, - ExceptionThrown, JsStackTrace, TerminationError, Throwable, + catch_exception, exception_already_thrown, log_traceback, CanContinue, ErrorOrException, ExcResult, + ExceptionThrown, Throwable, }; use self::ser::serialize_to_js; use self::string::{str_from_ident, IntoJsString}; @@ -52,8 +52,8 @@ use tokio::sync::oneshot; use tracing::Instrument; use v8::script_compiler::{compile_module, Source}; use v8::{ - scope_with_context, Context, Function, Isolate, Local, MapFnTo, OwnedIsolate, PinScope, ResolveModuleCallback, - ScriptOrigin, Value, + scope_with_context, ArrayBuffer, Context, Function, Isolate, Local, MapFnTo, OwnedIsolate, PinScope, + ResolveModuleCallback, ScriptOrigin, Value, }; mod budget; @@ -615,10 +615,16 @@ async fn spawn_instance_worker( let instance_env = InstanceEnv::new(replica_ctx.clone(), scheduler); scope.set_slot(JsInstanceEnv::new(instance_env)); + // Create a zero-initialized buffer for holding reducer args. + // Arguments needing more space will not use this. + const REDUCER_ARGS_BUFFER_SIZE: usize = 4_096; // 1 page. + let reducer_args_buf = ArrayBuffer::new(scope, REDUCER_ARGS_BUFFER_SIZE); + let mut inst = V8Instance { scope, replica_ctx, hooks: &hooks, + reducer_args_buf, }; // Process requests to the worker. @@ -807,7 +813,8 @@ fn call_free_fun<'scope>( struct V8Instance<'a, 'scope, 'isolate> { scope: &'a mut PinScope<'scope, 'isolate>, replica_ctx: &'a Arc, - hooks: &'a HookFunctions<'a>, + hooks: &'a HookFunctions<'scope>, + reducer_args_buf: Local<'scope, ArrayBuffer>, } impl WasmInstance for V8Instance<'_, '_, '_> { @@ -825,7 +832,7 @@ impl WasmInstance for V8Instance<'_, '_, '_> { fn call_reducer(&mut self, op: ReducerOp<'_>, budget: FunctionBudget) -> ReducerExecuteResult { common_call(self.scope, budget, op, |scope, op| { - Ok(call_call_reducer(scope, self.hooks, op)?) + Ok(call_call_reducer(scope, self.hooks, op, self.reducer_args_buf)?) }) .map_result(|call_result| call_result.and_then(|res| res.map_err(ExecutionError::User))) } @@ -975,7 +982,8 @@ mod test { timestamp: Timestamp::from_micros_since_unix_epoch(24), args: &ArgsTuple::nullary(), }; - Ok(call_call_reducer(scope, &hooks, op)?) + let buffer = v8::ArrayBuffer::new(scope, 4096); + Ok(call_call_reducer(scope, &hooks, op, buffer)?) }) }; diff --git a/crates/core/src/host/v8/syscall/common.rs b/crates/core/src/host/v8/syscall/common.rs new file mode 100644 index 00000000000..b3de098f645 --- /dev/null +++ b/crates/core/src/host/v8/syscall/common.rs @@ -0,0 +1,660 @@ +use super::super::{ + call_free_fun, + de::deserialize_js, + de::scratch_buf, + env_on_isolate, + error::{ + exception_already_thrown, no_such_iter, CodeError, ErrorOrException, ExceptionThrown, JsStackTrace, + SysCallError, SysCallResult, Throwable, TypeError, + }, + from_value::cast, + ser::serialize_to_js, + util::make_uint8array, + JsInstanceEnv, +}; +use super::HookFunctions; +use crate::host::instance_env::InstanceEnv; +use crate::host::wasm_common::{RowIterIdx, TimingSpan, TimingSpanIdx}; +use crate::{ + database_logger::{LogLevel, Record}, + host::wasm_common::module_host_actor::ProcedureOp, +}; +use anyhow::Context; +use bytes::Bytes; +use spacetimedb_lib::{ConnectionId, Identity, RawModuleDef, Timestamp}; +use spacetimedb_primitives::{errno, ColId, IndexId, ProcedureId, TableId}; +use spacetimedb_sats::bsatn; +use v8::{FunctionCallbackArguments, Isolate, Local, PinScope, Value}; + +/// Calls the `__call_procedure__` function `fun`. +pub fn call_call_procedure( + scope: &mut PinScope<'_, '_>, + hooks: &HookFunctions<'_>, + op: ProcedureOp, +) -> Result> { + let fun = hooks.call_procedure.context("`__call_procedure__` was never defined")?; + + let ProcedureOp { + id: ProcedureId(procedure_id), + name: _, + caller_identity: sender, + caller_connection_id: connection_id, + timestamp, + arg_bytes: procedure_args, + } = op; + // Serialize the arguments. + let procedure_id = serialize_to_js(scope, &procedure_id)?; + let sender = serialize_to_js(scope, &sender.to_u256())?; + let connection_id = serialize_to_js(scope, &connection_id.to_u128())?; + let timestamp = serialize_to_js(scope, ×tamp.to_micros_since_unix_epoch())?; + let procedure_args = serialize_to_js(scope, &procedure_args)?; + let args = &[procedure_id, sender, connection_id, timestamp, procedure_args]; + + // Call the function. + let ret = call_free_fun(scope, fun, args)?; + + // Deserialize the user result. + let ret = + cast!(scope, ret, v8::Uint8Array, "bytes return from `__call_procedure__`").map_err(|e| e.throw(scope))?; + let bytes = ret.get_contents(&mut []); + + Ok(Bytes::copy_from_slice(bytes)) +} + +/// Calls the registered `__describe_module__` function hook. +pub fn call_describe_module( + scope: &mut PinScope<'_, '_>, + hooks: &HookFunctions<'_>, +) -> Result> { + // Call the function. + let raw_mod_js = call_free_fun(scope, hooks.describe_module, &[])?; + + // Deserialize the raw module. + let raw_mod = cast!( + scope, + raw_mod_js, + v8::Uint8Array, + "bytes return from `__describe_module__`" + ) + .map_err(|e| e.throw(scope))?; + + let bytes = raw_mod.get_contents(&mut []); + let module = bsatn::from_slice::(bytes).context("invalid bsatn module def")?; + Ok(module) +} + +/// Returns the environment or errors. +pub fn get_env(isolate: &mut Isolate) -> SysCallResult<&mut JsInstanceEnv> { + env_on_isolate(isolate).ok_or(SysCallError::NoEnv) +} + +/// Module ABI that finds the `TableId` for a table name. +/// +/// # Signature +/// +/// ```ignore +/// table_id_from_name(name: string) -> u32 throws { +/// __code_error__: NOT_IN_TRANSACTION | NO_SUCH_TABLE +/// } +/// ``` +/// +/// # Types +/// +/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// +/// # Returns +/// +/// Returns an `u32` containing the id of the table. +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] +/// when called outside of a transaction. +/// +/// - [`spacetimedb_primitives::errno::NO_SUCH_TABLE`] +/// when `name` is not the name of a table. +/// +/// Throws a `TypeError` if: +/// - `name` is not `string`. +pub fn table_id_from_name(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { + let name: String = deserialize_js(scope, args.get(0))?; + Ok(get_env(scope)?.instance_env.table_id_from_name(&name)?) +} + +/// Module ABI that finds the `IndexId` for an index name. +/// +/// # Signature +/// +/// ```ignore +/// index_id_from_name(name: string) -> u32 throws { +/// __code_error__: NOT_IN_TRANSACTION | NO_SUCH_INDEX +/// } +/// ``` +/// +/// # Types +/// +/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// +/// # Returns +/// +/// Returns an `u32` containing the id of the index. +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] +/// when called outside of a transaction. +/// +/// - [`spacetimedb_primitives::errno::NO_SUCH_INDEX`] +/// when `name` is not the name of an index. +/// +/// Throws a `TypeError`: +/// - if `name` is not `string`. +pub fn index_id_from_name(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { + let name: String = deserialize_js(scope, args.get(0))?; + Ok(get_env(scope)?.instance_env.index_id_from_name(&name)?) +} + +/// Module ABI that returns the number of rows currently in table identified by `table_id`. +/// +/// # Signature +/// +/// ```ignore +/// datastore_table_row_count(table_id: u32) -> u64 throws { +/// __code_error__: NOT_IN_TRANSACTION | NO_SUCH_TABLE +/// } +/// ``` +/// +/// # Types +/// +/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// - `u64` is `bigint` in JS restricted to unsigned 64-bit integers. +/// +/// # Returns +/// +/// Returns a `u64` containing the number of rows in the table. +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] +/// when called outside of a transaction. +/// +/// - [`spacetimedb_primitives::errno::NO_SUCH_TABLE`] +/// when `table_id` is not a known ID of a table. +/// +/// Throws a `TypeError` if: +/// - `table_id` is not a `u32`. +pub fn datastore_table_row_count( + scope: &mut PinScope<'_, '_>, + args: FunctionCallbackArguments<'_>, +) -> SysCallResult { + let table_id: TableId = deserialize_js(scope, args.get(0))?; + Ok(get_env(scope)?.instance_env.datastore_table_row_count(table_id)?) +} + +/// Module ABI that starts iteration on each row, as BSATN-encoded, +/// of a table identified by `table_id`. +/// +/// # Signature +/// +/// ```ignore +/// datastore_table_scan_bsatn(table_id: u32) -> u32 throws { +/// __code_error__: NOT_IN_TRANSACTION | NO_SUCH_TABLE +/// } +/// ``` +/// +/// # Types +/// +/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// - `u64` is `bigint` in JS restricted to unsigned 64-bit integers. +/// +/// # Returns +/// +/// Returns a `u32` that is the iterator handle. +/// This handle can be advanced by [`row_iter_bsatn_advance`]. +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] +/// when called outside of a transaction. +/// +/// - [`spacetimedb_primitives::errno::NO_SUCH_TABLE`] +/// when `table_id` is not a known ID of a table. +/// +/// Throws a `TypeError`: +/// - if `table_id` is not a `u32`. +pub fn datastore_table_scan_bsatn( + scope: &mut PinScope<'_, '_>, + args: FunctionCallbackArguments<'_>, +) -> SysCallResult { + let table_id: TableId = deserialize_js(scope, args.get(0))?; + + let env = get_env(scope)?; + // Collect the iterator chunks. + let chunks = env + .instance_env + .datastore_table_scan_bsatn_chunks(&mut env.chunk_pool, table_id)?; + + // Register the iterator and get back the index to write to `out`. + // Calls to the iterator are done through dynamic dispatch. + Ok(env.iters.insert(chunks.into_iter()).0) +} + +/// This is a helper function that is used by +/// `v1::datastore_index_scan_range_bsatn` +/// and `v2::datastore_index_scan_range_bsatn`. +/// See those for additional details. +pub fn datastore_index_scan_range_bsatn_inner( + scope: &mut PinScope<'_, '_>, + index_id: IndexId, + mut prefix: &[u8], + prefix_elems: ColId, + rstart: &[u8], + rend: &[u8], +) -> SysCallResult { + if prefix_elems.idx() == 0 { + prefix = &[]; + } + + let env = get_env(scope)?; + + // Find the relevant rows. + let chunks = env.instance_env.datastore_index_scan_range_bsatn_chunks( + &mut env.chunk_pool, + index_id, + prefix, + prefix_elems, + rstart, + rend, + )?; + + // Insert the encoded + concatenated rows into a new buffer and return its id. + Ok(env.iters.insert(chunks.into_iter()).0) +} + +pub fn deserialize_row_iter_idx(scope: &mut PinScope<'_, '_>, value: Local<'_, Value>) -> SysCallResult { + deserialize_js(scope, value).map(RowIterIdx).map_err(Into::into) +} + +/// Module ABI that destroys the iterator registered under `iter`. +/// +/// Once `row_iter_bsatn_close` is called on `iter`, the `iter` is invalid. +/// That is, `row_iter_bsatn_close(iter)` the second time will yield `NO_SUCH_ITER`. +/// +/// # Signature +/// +/// ```ignore +/// row_iter_bsatn_close(iter: u32) -> undefined throws { +/// __code_error__: NO_SUCH_ITER +/// } +/// ``` +/// +/// # Types +/// +/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// +/// # Returns +/// +/// Returns nothing. +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NO_SUCH_ITER`] +/// when `iter` is not a valid iterator. +/// +/// Throws a `TypeError` if: +/// - `iter` is not a `u32`. +pub fn row_iter_bsatn_close<'scope>( + scope: &mut PinScope<'scope, '_>, + args: FunctionCallbackArguments<'scope>, +) -> SysCallResult<()> { + let row_iter_idx: u32 = deserialize_js(scope, args.get(0))?; + let row_iter_idx = RowIterIdx(row_iter_idx); + + // Retrieve the iterator by `row_iter_idx`, or error. + let env = get_env(scope)?; + + // Retrieve the iterator by `row_iter_idx`, or error. + if env.iters.take(row_iter_idx).is_none() { + return Err(no_such_iter(scope)); + } else { + // TODO(Centril): consider putting these into a pool for reuse. + } + + Ok(()) +} + +/// # Signature +/// +/// ```ignore +/// volatile_nonatomic_schedule_immediate(reducer_name: string, args: u8[]) -> undefined +/// ``` +pub fn volatile_nonatomic_schedule_immediate<'scope>( + scope: &mut PinScope<'scope, '_>, + args: FunctionCallbackArguments<'scope>, +) -> SysCallResult<()> { + let name: String = deserialize_js(scope, args.get(0))?; + let args: Vec = deserialize_js(scope, args.get(1))?; + + get_env(scope)? + .instance_env + .scheduler + .volatile_nonatomic_schedule_immediate(name, crate::host::FunctionArgs::Bsatn(args.into())); + + Ok(()) +} + +/// Module ABI that logs at `level` a `message` message occurring +/// at the parent stack frame. +/// +/// The `message` is interpreted lossily as a UTF-8 string. +/// +/// # Signature +/// +/// ```ignore +/// console_log(level: u8, message: string) -> u32 +/// ``` +/// +/// # Types +/// +/// - `u8` is `number` in JS restricted to unsigned 8-bit integers. +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// +/// # Returns +/// +/// Returns nothing. +pub fn console_log<'scope>( + scope: &mut PinScope<'scope, '_>, + args: FunctionCallbackArguments<'scope>, +) -> SysCallResult<()> { + let level: u32 = deserialize_js(scope, args.get(0))?; + + let msg = args.get(1).cast::(); + let mut buf = scratch_buf::<128>(); + let msg = msg.to_rust_cow_lossy(scope, &mut buf); + + let frame: Local<'_, v8::StackFrame> = v8::StackTrace::current_stack_trace(scope, 2) + .ok_or_else(exception_already_thrown)? + .get_frame(scope, 1) + .ok_or_else(exception_already_thrown)?; + let mut buf = scratch_buf::<32>(); + let filename = frame + .get_script_name(scope) + .map(|s| s.to_rust_cow_lossy(scope, &mut buf)); + + let level = (level as u8).into(); + let trace = if level == LogLevel::Panic { + JsStackTrace::from_current_stack_trace(scope)? + } else { + <_>::default() + }; + + let env = get_env(scope).inspect_err(|_| { + tracing::warn!( + "{}:{} {msg}", + filename.as_deref().unwrap_or("unknown"), + frame.get_line_number() + ); + })?; + + let function = env.log_record_function(); + let record = Record { + // TODO: figure out whether to use walltime now or logical reducer now (env.reducer_start) + ts: InstanceEnv::now_for_logging(), + target: None, + filename: filename.as_deref(), + line_number: Some(frame.get_line_number() as u32), + function, + message: &msg, + }; + + env.instance_env.console_log(level, &record, &trace); + + Ok(()) +} + +/// Module ABI that begins a timing span with `name`. +/// +/// When the returned `ConsoleTimerId` is passed to [`console_timer_end`], +/// the duration between the calls will be printed to the module's logs. +/// +/// The `name` is interpreted lossily as a UTF-8 string. +/// +/// # Signature +/// +/// ```ignore +/// console_timer_start(name: string) -> u32 +/// ``` +/// +/// # Types +/// +/// - `u8` is `number` in JS restricted to unsigned 8-bit integers. +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// +/// # Returns +/// +/// Returns a `u32` that is the `ConsoleTimerId`. +/// +/// # Throws +/// +/// Throws a `TypeError` if: +/// - `name` is not a `string`. +pub fn console_timer_start<'scope>( + scope: &mut PinScope<'scope, '_>, + args: FunctionCallbackArguments<'scope>, +) -> SysCallResult { + let name = args.get(0).cast::(); + let mut buf = scratch_buf::<128>(); + let name = name.to_rust_cow_lossy(scope, &mut buf).into_owned(); + + let span_id = get_env(scope)?.timing_spans.insert(TimingSpan::new(name)).0; + Ok(span_id) +} + +/// Module ABI that ends a timing span with `span_id`. +/// +/// # Signature +/// +/// ```ignore +/// console_timer_end(span_id: u32) -> undefined throws { +/// __code_error__: NO_SUCH_CONSOLE_TIMER +/// } +/// ``` +/// +/// # Types +/// +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// +/// # Returns +/// +/// Returns nothing. +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NO_SUCH_CONSOLE_TIMER`] +/// when `span_id` doesn't refer to an active timing span. +/// +/// Throws a `TypeError` if: +/// - `span_id` is not a `u32`. +pub fn console_timer_end<'scope>( + scope: &mut PinScope<'scope, '_>, + args: FunctionCallbackArguments<'scope>, +) -> SysCallResult<()> { + let span_id: u32 = deserialize_js(scope, args.get(0))?; + + let env = get_env(scope)?; + let Some(span) = env.timing_spans.take(TimingSpanIdx(span_id)) else { + let exc = CodeError::from_code(scope, errno::NO_SUCH_CONSOLE_TIMER.get())?; + return Err(exc.throw(scope).into()); + }; + let function = env.log_record_function(); + env.instance_env.console_timer_end(&span, function); + + Ok(()) +} + +/// Module ABI to read a JWT payload associated with a connection ID from the system tables. +/// +/// # Signature +/// +/// ```ignore +/// get_jwt_payload(connection_id: u128) -> u8[] throws { +/// __code_error__: +/// NOT_IN_TRANSACTION +/// } +/// ``` +/// +/// # Types +/// +/// - `u128` is `bigint` in JS restricted to unsigned 128-bit integers. +/// +/// # Returns +/// +/// Returns a byte array encoding the JWT payload if one is found. If one is not found, an +/// empty byte array is returned. +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] +/// when called outside of a transaction. +pub fn get_jwt_payload(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult> { + let connection_id: u128 = deserialize_js(scope, args.get(0))?; + let connection_id = ConnectionId::from_u128(connection_id); + let payload = get_env(scope)? + .instance_env + .get_jwt_payload(connection_id)? + .map(String::into_bytes) + .unwrap_or_default(); + Ok(payload) +} + +/// Module ABI that returns the module identity. +/// +/// # Signature +/// +/// ```ignore +/// identity() -> { __identity__: u256 } +/// ``` +/// +/// # Types +/// +/// - `u256` is `bigint` in JS restricted to unsigned 256-bit integers. +/// +/// # Returns +/// +/// Returns the module identity. +pub fn identity<'scope>( + scope: &mut PinScope<'scope, '_>, + _: FunctionCallbackArguments<'scope>, +) -> SysCallResult { + Ok(*get_env(scope)?.instance_env.database_identity()) +} + +/// Execute an HTTP request in the context of a procedure. +/// +/// # Signature +/// +/// ```ignore +/// function procedure_http_request( +/// request: Uint8Array, +/// body: Uint8Array | string +/// ): [response: Uint8Array, body: Uint8Array]; +/// ``` +/// +/// Accepts a BSATN-encoded [`spacetimedb_lib::http::Request`] and a request body, and +/// returns a BSATN-encoded [`spacetimedb_lib::http::Response`] and the response body. +pub fn procedure_http_request<'scope>( + scope: &mut PinScope<'scope, '_>, + args: FunctionCallbackArguments<'scope>, +) -> SysCallResult> { + use spacetimedb_lib::http as st_http; + + let request = + cast!(scope, args.get(0), v8::Uint8Array, "Uint8Array for procedure request").map_err(|e| e.throw(scope))?; + + let request = bsatn::from_slice::(request.get_contents(&mut [])) + .map_err(|e| TypeError(format!("failed to decode http request: {e}")).throw(scope))?; + + let request_body = args.get(1); + let request_body = if let Ok(s) = request_body.try_cast::() { + Bytes::from(s.to_rust_string_lossy(scope)) + } else { + let bytes = cast!( + scope, + request_body, + v8::Uint8Array, + "Uint8Array or string for request body" + ) + .map_err(|e| e.throw(scope))?; + Bytes::copy_from_slice(bytes.get_contents(&mut [])) + }; + + let env = get_env(scope)?; + + let fut = env.instance_env.http_request(request, request_body)?; + + let rt = tokio::runtime::Handle::current(); + let (response, response_body) = rt.block_on(fut)?; + + let response = bsatn::to_vec(&response).expect("failed to serialize `HttpResponse`"); + let response = make_uint8array(scope, response); + + let response_body = match response_body.try_into_mut() { + Ok(bytes_mut) => make_uint8array(scope, Box::new(bytes_mut)), + Err(bytes) => make_uint8array(scope, Vec::from(bytes)), + }; + + Ok(v8::Array::new_with_elements( + scope, + &[response.into(), response_body.into()], + )) +} + +pub fn procedure_start_mut_tx( + scope: &mut PinScope<'_, '_>, + _args: FunctionCallbackArguments<'_>, +) -> SysCallResult { + let env = get_env(scope)?; + + env.instance_env.start_mutable_tx()?; + + let timestamp = Timestamp::now().to_micros_since_unix_epoch() as u64; + + Ok(timestamp) +} + +pub fn procedure_abort_mut_tx(scope: &mut PinScope<'_, '_>, _args: FunctionCallbackArguments<'_>) -> SysCallResult<()> { + let env = get_env(scope)?; + + env.instance_env.abort_mutable_tx()?; + Ok(()) +} + +pub fn procedure_commit_mut_tx( + scope: &mut PinScope<'_, '_>, + _args: FunctionCallbackArguments<'_>, +) -> SysCallResult<()> { + let env = get_env(scope)?; + + env.instance_env.commit_mutable_tx()?; + + Ok(()) +} diff --git a/crates/core/src/host/v8/syscall/mod.rs b/crates/core/src/host/v8/syscall/mod.rs index ed7fa8f4cb2..8175397f829 100644 --- a/crates/core/src/host/v8/syscall/mod.rs +++ b/crates/core/src/host/v8/syscall/mod.rs @@ -1,16 +1,14 @@ -use bytes::Bytes; -use spacetimedb_lib::{RawModuleDef, VersionTuple}; -use v8::{callback_scope, Context, FixedArray, Local, Module, PinScope}; - use crate::host::v8::de::scratch_buf; use crate::host::v8::error::{ErrorOrException, ExcResult, ExceptionThrown, Throwable, TypeError}; use crate::host::wasm_common::abi::parse_abi_version; -use crate::host::wasm_common::module_host_actor::{ - AnonymousViewOp, ProcedureOp, ReducerOp, ReducerResult, ViewOp, ViewReturnData, -}; +use crate::host::wasm_common::module_host_actor::{AnonymousViewOp, ReducerOp, ReducerResult, ViewOp, ViewReturnData}; +use spacetimedb_lib::VersionTuple; +use v8::{callback_scope, ArrayBuffer, Context, FixedArray, Local, Module, PinScope}; +mod common; mod hooks; mod v1; +mod v2; pub(super) use self::hooks::{get_hooks, HookFunctions, ModuleHookKey}; @@ -21,6 +19,7 @@ pub(super) type FnRet<'scope> = ExcResult>; #[derive(Copy, Clone, PartialEq, Eq)] pub enum AbiVersion { V1, + V2, } /// A dependency resolver for the user's module @@ -58,9 +57,10 @@ fn resolve_sys_module_inner<'scope>( (1, 1) => Ok(v1::sys_v1_1(scope)), (1, 2) => Ok(v1::sys_v1_2(scope)), (1, 3) => Ok(v1::sys_v1_3(scope)), + (2, 0) => Ok(v2::sys_v2_0(scope)), _ => Err(TypeError(format!( "Could not import {spec:?}, likely because this module was built for a newer version of SpacetimeDB.\n\ - It requires sys module v{major}.{minor}, but that version is not supported by the database." + It requires sys module v{major}.{minor}, but that version is not supported by the database." )) .throw(scope)), }, @@ -71,13 +71,15 @@ fn resolve_sys_module_inner<'scope>( /// Calls the registered `__call_reducer__` function hook. /// /// This handles any (future) ABI version differences. -pub(super) fn call_call_reducer( - scope: &mut PinScope<'_, '_>, - hooks: &HookFunctions<'_>, +pub(super) fn call_call_reducer<'scope>( + scope: &mut PinScope<'scope, '_>, + hooks: &HookFunctions<'scope>, op: ReducerOp<'_>, + reducer_args_buf: Local<'scope, ArrayBuffer>, ) -> ExcResult { match hooks.abi { AbiVersion::V1 => v1::call_call_reducer(scope, hooks, op), + AbiVersion::V2 => v2::call_call_reducer(scope, hooks, op, reducer_args_buf), } } @@ -91,6 +93,7 @@ pub(super) fn call_call_view( ) -> Result> { match hooks.abi { AbiVersion::V1 => v1::call_call_view(scope, hooks, op), + AbiVersion::V2 => v2::call_call_view(scope, hooks, op), } } @@ -104,30 +107,8 @@ pub(super) fn call_call_view_anon( ) -> Result> { match hooks.abi { AbiVersion::V1 => v1::call_call_view_anon(scope, hooks, op), + AbiVersion::V2 => v2::call_call_view_anon(scope, hooks, op), } } -/// Calls the registered `__call_procedure__` function hook. -/// -/// This handles any (future) ABI version differences. -pub(super) fn call_call_procedure( - scope: &mut PinScope<'_, '_>, - hooks: &HookFunctions<'_>, - op: ProcedureOp, -) -> Result> { - match hooks.abi { - AbiVersion::V1 => v1::call_call_procedure(scope, hooks, op), - } -} - -/// Calls the registered `__describe_module__` function hook. -/// -/// This handles any (future) ABI version differences. -pub(super) fn call_describe_module<'scope>( - scope: &mut PinScope<'scope, '_>, - hooks: &HookFunctions<'_>, -) -> Result> { - match hooks.abi { - AbiVersion::V1 => v1::call_describe_module(scope, hooks), - } -} +pub use self::common::{call_call_procedure, call_describe_module}; diff --git a/crates/core/src/host/v8/syscall/v1.rs b/crates/core/src/host/v8/syscall/v1.rs index a7a90272352..113d436fba7 100644 --- a/crates/core/src/host/v8/syscall/v1.rs +++ b/crates/core/src/host/v8/syscall/v1.rs @@ -1,33 +1,32 @@ +use super::super::de::deserialize_js; +use super::super::error::{ + handle_sys_call_error, no_such_iter, throw_if_terminated, BufferTooSmall, ErrorOrException, ExcResult, + ExceptionThrown, SysCallResult, +}; +use super::super::from_value::cast; +use super::super::ser::serialize_to_js; +use super::super::string::{str_from_ident, StringConst}; +use super::super::{call_free_fun, env_on_isolate, Throwable}; +use super::common::{ + console_log, console_timer_end, console_timer_start, datastore_index_scan_range_bsatn_inner, + datastore_table_row_count, datastore_table_scan_bsatn, deserialize_row_iter_idx, get_env, get_jwt_payload, + identity, index_id_from_name, procedure_abort_mut_tx, procedure_commit_mut_tx, procedure_http_request, + procedure_start_mut_tx, row_iter_bsatn_close, table_id_from_name, volatile_nonatomic_schedule_immediate, +}; +use super::hooks::HookFunctions; use super::hooks::{get_hook_function, set_hook_slots}; use super::{AbiVersion, FnRet, ModuleHookKey}; -use crate::database_logger::{LogLevel, Record}; -use crate::error::NodesError; use crate::host::instance_env::InstanceEnv; -use crate::host::v8::de::{deserialize_js, scratch_buf}; -use crate::host::v8::error::{CodeError, CodeMessageError, ErrorOrException, ExcResult, ExceptionThrown, TypeError}; -use crate::host::v8::from_value::cast; -use crate::host::v8::ser::serialize_to_js; -use crate::host::v8::string::{str_from_ident, StringConst}; -use crate::host::v8::syscall::hooks::HookFunctions; -use crate::host::v8::util::make_uint8array; -use crate::host::v8::{ - call_free_fun, env_on_isolate, exception_already_thrown, BufferTooSmall, JsInstanceEnv, JsStackTrace, - TerminationError, Throwable, -}; use crate::host::wasm_common::instrumentation::span; -use crate::host::wasm_common::module_host_actor::{ - AnonymousViewOp, ProcedureOp, ReducerOp, ReducerResult, ViewOp, ViewReturnData, -}; -use crate::host::wasm_common::{err_to_errno_and_log, RowIterIdx, TimingSpan, TimingSpanIdx}; +use crate::host::wasm_common::module_host_actor::{AnonymousViewOp, ReducerOp, ReducerResult, ViewOp, ViewReturnData}; use crate::host::AbiCall; use anyhow::Context; use bytes::Bytes; -use spacetimedb_lib::{bsatn, ConnectionId, Identity, RawModuleDef, Timestamp}; -use spacetimedb_primitives::{errno, ColId, IndexId, ProcedureId, ReducerId, TableId, ViewFnPtr}; +use spacetimedb_primitives::{ColId, IndexId, ReducerId, TableId, ViewFnPtr}; use spacetimedb_sats::Serialize; use v8::{ - callback_scope, ConstructorBehavior, Function, FunctionCallbackArguments, Isolate, Local, Module, Object, - PinCallbackScope, PinScope, + callback_scope, ConstructorBehavior, Function, FunctionCallbackArguments, Local, Module, Object, PinCallbackScope, + PinScope, }; macro_rules! create_synthetic_module { @@ -133,7 +132,7 @@ pub(super) fn sys_v1_2<'scope>(scope: &mut PinScope<'scope, '_>) -> Local<'scope procedure_http_request ), ( - with_sys_result_value, + with_sys_result_ret, AbiCall::ProcedureStartMutTransaction, procedure_start_mut_tx ), @@ -186,23 +185,12 @@ fn register_module_fun( module.set_synthetic_module_export(scope, name, fun) } -/// A flag set in [`handle_nodes_error`]. -/// The flag should be checked in every module -> host ABI. -/// If the flag is set, the call is prevented. -struct TerminationFlag; - /// Adapts `fun`, which returns a [`Value`] to one that works on [`v8::ReturnValue`]. fn adapt_fun( fun: impl Copy + for<'scope> Fn(&mut PinScope<'scope, '_>, FunctionCallbackArguments<'scope>) -> FnRet<'scope>, ) -> impl Copy + for<'scope> Fn(&mut PinScope<'scope, '_>, FunctionCallbackArguments<'scope>, v8::ReturnValue) { move |scope, args, mut rv| { - // If the flag was set in `handle_nodes_error`, - // we need to block all module -> host ABI calls. - if scope.get_slot::().is_some() { - let err = anyhow::anyhow!("execution is being terminated"); - if let Ok(exception) = TerminationError::from_error(scope, &err) { - exception.throw(scope); - } + if throw_if_terminated(scope) { return; } @@ -213,16 +201,6 @@ fn adapt_fun( } } -/// Either an exception, already thrown, or [`NodesError`] arising from [`InstanceEnv`]. -#[derive(derive_more::From)] -enum SysCallError { - NoEnv, - Error(NodesError), - Exception(ExceptionThrown), -} - -type SysCallResult = Result; - /// Wraps `run` in [`with_span`] and returns the return value of `run` to JS. /// Handles [`SysCallError`] if it occurs by throwing exceptions into JS. fn with_sys_result_ret<'scope, O: Serialize>( @@ -233,7 +211,7 @@ fn with_sys_result_ret<'scope, O: Serialize>( ) -> FnRet<'scope> { match with_span(abi_call, scope, args, run) { Ok(ret) => serialize_to_js(scope, &ret), - Err(err) => handle_sys_call_error(abi_call, scope, err), + Err(err) => Err(handle_sys_call_error(abi_call, scope, err)), } } @@ -247,7 +225,7 @@ fn with_sys_result_noret<'scope>( ) -> FnRet<'scope> { match with_span(abi_call, scope, args, run) { Ok(()) => Ok(v8::undefined(scope).into()), - Err(err) => handle_sys_call_error(abi_call, scope, err), + Err(err) => Err(handle_sys_call_error(abi_call, scope, err)), } } @@ -264,7 +242,7 @@ where { match with_span(abi_call, scope, args, run) { Ok(v) => Ok(v.into()), - Err(err) => handle_sys_call_error(abi_call, scope, err), + Err(err) => Err(handle_sys_call_error(abi_call, scope, err)), } } @@ -278,59 +256,6 @@ fn with_nothing<'scope>( run(scope, args) } -/// Converts a `SysCallError` into a `ExceptionThrown`. -fn handle_sys_call_error<'scope>( - abi_call: AbiCall, - scope: &mut PinScope<'scope, '_>, - err: SysCallError, -) -> FnRet<'scope> { - const ENV_NOT_SET: u16 = 1; - match err { - SysCallError::NoEnv => Err(code_error(scope, ENV_NOT_SET)), - SysCallError::Exception(exc) => Err(exc), - SysCallError::Error(error) => Err(throw_nodes_error(abi_call, scope, error)), - } -} - -/// Throws `{ __code_error__: code }`. -fn code_error(scope: &PinScope<'_, '_>, code: u16) -> ExceptionThrown { - let res = CodeError::from_code(scope, code); - collapse_exc_thrown(scope, res) -} - -/// Turns a [`NodesError`] into a thrown exception. -fn throw_nodes_error(abi_call: AbiCall, scope: &mut PinScope<'_, '_>, error: NodesError) -> ExceptionThrown { - let res = match err_to_errno_and_log::(abi_call, error) { - Ok((code, None)) => CodeError::from_code(scope, code), - Ok((code, Some(message))) => CodeMessageError::from_code(scope, code, message), - Err(err) => { - // Terminate execution ASAP and throw a catchable exception (`TerminationError`). - // Unfortunately, JS execution won't be terminated once the callback returns, - // so we set a slot that all callbacks immediately check - // to ensure that the module won't be able to do anything to the host - // while it's being terminated (eventually). - scope.terminate_execution(); - scope.set_slot(TerminationFlag); - TerminationError::from_error(scope, &err) - } - }; - collapse_exc_thrown(scope, res) -} - -/// Collapses `res` where the `Ok(x)` where `x` is throwable. -fn collapse_exc_thrown<'scope>( - scope: &PinScope<'scope, '_>, - res: ExcResult>, -) -> ExceptionThrown { - let (Ok(thrown) | Err(thrown)) = res.map(|ev| ev.throw(scope)); - thrown -} - -/// Returns the environment or errors. -fn get_env(isolate: &mut Isolate) -> Result<&mut JsInstanceEnv, SysCallError> { - env_on_isolate(isolate).ok_or(SysCallError::NoEnv) -} - /// Tracks the span of `body` under the label `abi_call`. fn with_span<'scope, T, E: From>( abi_call: AbiCall, @@ -641,220 +566,6 @@ pub(super) fn call_call_view_anon( Ok(ViewReturnData::HeaderFirst(Bytes::copy_from_slice(bytes))) } -/// Calls the `__call_procedure__` function `fun`. -pub(super) fn call_call_procedure( - scope: &mut PinScope<'_, '_>, - hooks: &HookFunctions<'_>, - op: ProcedureOp, -) -> Result> { - let fun = hooks.call_procedure.context("`__call_procedure__` was never defined")?; - - let ProcedureOp { - id: ProcedureId(procedure_id), - name: _, - caller_identity: sender, - caller_connection_id: connection_id, - timestamp, - arg_bytes: procedure_args, - } = op; - // Serialize the arguments. - let procedure_id = serialize_to_js(scope, &procedure_id)?; - let sender = serialize_to_js(scope, &sender.to_u256())?; - let connection_id = serialize_to_js(scope, &connection_id.to_u128())?; - let timestamp = serialize_to_js(scope, ×tamp.to_micros_since_unix_epoch())?; - let procedure_args = serialize_to_js(scope, &procedure_args)?; - let args = &[procedure_id, sender, connection_id, timestamp, procedure_args]; - - // Call the function. - let ret = call_free_fun(scope, fun, args)?; - - // Deserialize the user result. - let ret = - cast!(scope, ret, v8::Uint8Array, "bytes return from `__call_procedure__`").map_err(|e| e.throw(scope))?; - let bytes = ret.get_contents(&mut []); - - Ok(Bytes::copy_from_slice(bytes)) -} - -/// Calls the registered `__describe_module__` function hook. -pub(super) fn call_describe_module( - scope: &mut PinScope<'_, '_>, - hooks: &HookFunctions<'_>, -) -> Result> { - // Call the function. - let raw_mod_js = call_free_fun(scope, hooks.describe_module, &[])?; - - // Deserialize the raw module. - let raw_mod = cast!( - scope, - raw_mod_js, - v8::Uint8Array, - "bytes return from `__describe_module__`" - ) - .map_err(|e| e.throw(scope))?; - - let bytes = raw_mod.get_contents(&mut []); - let module = bsatn::from_slice::(bytes).context("invalid bsatn module def")?; - Ok(module) -} - -/// Module ABI that finds the `TableId` for a table name. -/// -/// # Signature -/// -/// ```ignore -/// table_id_from_name(name: string) -> u32 throws { -/// __code_error__: NOT_IN_TRANSACTION | NO_SUCH_TABLE -/// } -/// ``` -/// -/// # Types -/// -/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. -/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. -/// -/// # Returns -/// -/// Returns an `u32` containing the id of the table. -/// -/// # Throws -/// -/// Throws `{ __code_error__: u16 }` where `__code_error__` is: -/// -/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] -/// when called outside of a transaction. -/// -/// - [`spacetimedb_primitives::errno::NO_SUCH_TABLE`] -/// when `name` is not the name of a table. -/// -/// Throws a `TypeError` if: -/// - `name` is not `string`. -fn table_id_from_name(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { - let name: String = deserialize_js(scope, args.get(0))?; - Ok(get_env(scope)?.instance_env.table_id_from_name(&name)?) -} - -/// Module ABI that finds the `IndexId` for an index name. -/// -/// # Signature -/// -/// ```ignore -/// index_id_from_name(name: string) -> u32 throws { -/// __code_error__: NOT_IN_TRANSACTION | NO_SUCH_INDEX -/// } -/// ``` -/// -/// # Types -/// -/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. -/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. -/// -/// # Returns -/// -/// Returns an `u32` containing the id of the index. -/// -/// # Throws -/// -/// Throws `{ __code_error__: u16 }` where `__code_error__` is: -/// -/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] -/// when called outside of a transaction. -/// -/// - [`spacetimedb_primitives::errno::NO_SUCH_INDEX`] -/// when `name` is not the name of an index. -/// -/// Throws a `TypeError`: -/// - if `name` is not `string`. -fn index_id_from_name(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { - let name: String = deserialize_js(scope, args.get(0))?; - Ok(get_env(scope)?.instance_env.index_id_from_name(&name)?) -} - -/// Module ABI that returns the number of rows currently in table identified by `table_id`. -/// -/// # Signature -/// -/// ```ignore -/// datastore_table_row_count(table_id: u32) -> u64 throws { -/// __code_error__: NOT_IN_TRANSACTION | NO_SUCH_TABLE -/// } -/// ``` -/// -/// # Types -/// -/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. -/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. -/// - `u64` is `bigint` in JS restricted to unsigned 64-bit integers. -/// -/// # Returns -/// -/// Returns a `u64` containing the number of rows in the table. -/// -/// # Throws -/// -/// Throws `{ __code_error__: u16 }` where `__code_error__` is: -/// -/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] -/// when called outside of a transaction. -/// -/// - [`spacetimedb_primitives::errno::NO_SUCH_TABLE`] -/// when `table_id` is not a known ID of a table. -/// -/// Throws a `TypeError` if: -/// - `table_id` is not a `u32`. -fn datastore_table_row_count(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { - let table_id: TableId = deserialize_js(scope, args.get(0))?; - Ok(get_env(scope)?.instance_env.datastore_table_row_count(table_id)?) -} - -/// Module ABI that starts iteration on each row, as BSATN-encoded, -/// of a table identified by `table_id`. -/// -/// # Signature -/// -/// ```ignore -/// datastore_table_scan_bsatn(table_id: u32) -> u32 throws { -/// __code_error__: NOT_IN_TRANSACTION | NO_SUCH_TABLE -/// } -/// ``` -/// -/// # Types -/// -/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. -/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. -/// - `u64` is `bigint` in JS restricted to unsigned 64-bit integers. -/// -/// # Returns -/// -/// Returns a `u32` that is the iterator handle. -/// This handle can be advanced by [`row_iter_bsatn_advance`]. -/// -/// # Throws -/// -/// Throws `{ __code_error__: u16 }` where `__code_error__` is: -/// -/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] -/// when called outside of a transaction. -/// -/// - [`spacetimedb_primitives::errno::NO_SUCH_TABLE`] -/// when `table_id` is not a known ID of a table. -/// -/// Throws a `TypeError`: -/// - if `table_id` is not a `u32`. -fn datastore_table_scan_bsatn(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { - let table_id: TableId = deserialize_js(scope, args.get(0))?; - - let env = get_env(scope)?; - // Collect the iterator chunks. - let chunks = env - .instance_env - .datastore_table_scan_bsatn_chunks(&mut env.chunk_pool, table_id)?; - - // Register the iterator and get back the index to write to `out`. - // Calls to the iterator are done through dynamic dispatch. - Ok(env.iters.insert(chunks.into_iter()).0) -} - /// Module ABI that finds all rows in the index identified by `index_id`, /// according to `prefix`, `rstart`, and `rend`. /// @@ -876,7 +587,7 @@ fn datastore_table_scan_bsatn(scope: &mut PinScope<'_, '_>, args: FunctionCallba /// The relevant table for the index is found implicitly via the `index_id`, /// which is unique for the module. /// -/// On success, the iterator handle is written to the `out` pointer. +/// On success, the iterator handle is returned. /// This handle can be advanced by [`row_iter_bsatn_advance`]. /// /// # Non-obvious queries @@ -936,7 +647,7 @@ fn datastore_table_scan_bsatn(scope: &mut PinScope<'_, '_>, args: FunctionCallba /// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. /// /// Throws a `TypeError` if: -/// - `table_id` is not a `u32`. +/// - `index_id` is not a `u32`. /// - `prefix`, `rstart`, and `rend` are not arrays of `u8`s. /// - `prefix_elems` is not a `u16`. fn datastore_index_scan_range_bsatn( @@ -944,34 +655,12 @@ fn datastore_index_scan_range_bsatn( args: FunctionCallbackArguments<'_>, ) -> SysCallResult { let index_id: IndexId = deserialize_js(scope, args.get(0))?; - let mut prefix: &[u8] = deserialize_js(scope, args.get(1))?; + let prefix: &[u8] = deserialize_js(scope, args.get(1))?; let prefix_elems: ColId = deserialize_js(scope, args.get(2))?; let rstart: &[u8] = deserialize_js(scope, args.get(3))?; let rend: &[u8] = deserialize_js(scope, args.get(4))?; - if prefix_elems.idx() == 0 { - prefix = &[]; - } - - let env = get_env(scope)?; - - // Find the relevant rows. - let chunks = env.instance_env.datastore_index_scan_range_bsatn_chunks( - &mut env.chunk_pool, - index_id, - prefix, - prefix_elems, - rstart, - rend, - )?; - - // Insert the encoded + concatenated rows into a new buffer and return its id. - Ok(env.iters.insert(chunks.into_iter()).0) -} - -/// Throws `{ __code_error__: NO_SUCH_ITER }`. -fn no_such_iter(scope: &PinScope<'_, '_>) -> SysCallError { - code_error(scope, errno::NO_SUCH_ITER.get()).into() + datastore_index_scan_range_bsatn_inner(scope, index_id, prefix, prefix_elems, rstart, rend) } /// Module ABI that reads rows from the given iterator registered under `iter`. @@ -1027,8 +716,7 @@ fn row_iter_bsatn_advance<'scope>( scope: &mut PinScope<'scope, '_>, args: FunctionCallbackArguments<'scope>, ) -> SysCallResult<(bool, Vec)> { - let row_iter_idx: u32 = deserialize_js(scope, args.get(0))?; - let row_iter_idx = RowIterIdx(row_iter_idx); + let row_iter_idx = deserialize_row_iter_idx(scope, args.get(0))?; let buffer_max_len: u32 = deserialize_js(scope, args.get(1))?; // Retrieve the iterator by `row_iter_idx`, or error. @@ -1043,71 +731,23 @@ fn row_iter_bsatn_advance<'scope>( let written = InstanceEnv::fill_buffer_from_iter(iter, &mut buffer, &mut env.chunk_pool); buffer.truncate(written); - match (written, iter.as_slice().first().map(|c| c.len().try_into().unwrap())) { + let next_buf_len = iter.as_slice().first().map(|v| v.len()); + let done = match (written, next_buf_len) { // Nothing was written and the iterator is not exhausted. (0, Some(min_len)) => { + let min_len = min_len.try_into().unwrap(); let exc = BufferTooSmall::from_requirement(scope, min_len)?; - Err(exc.throw(scope).into()) + return Err(exc.throw(scope).into()); } // The iterator is exhausted, destroy it, and tell the caller. (_, None) => { env.iters.take(row_iter_idx); - Ok((true, buffer)) + true } // Something was written, but the iterator is not exhausted. - (_, Some(_)) => Ok((false, buffer)), - } -} - -/// Module ABI that destroys the iterator registered under `iter`. -/// -/// Once `row_iter_bsatn_close` is called on `iter`, the `iter` is invalid. -/// That is, `row_iter_bsatn_close(iter)` the second time will yield `NO_SUCH_ITER`. -/// -/// # Signature -/// -/// ```ignore -/// row_iter_bsatn_close(iter: u32) -> undefined throws { -/// __code_error__: NO_SUCH_ITER -/// } -/// ``` -/// -/// # Types -/// -/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. -/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. -/// -/// # Returns -/// -/// Returns nothing. -/// -/// # Throws -/// -/// Throws `{ __code_error__: u16 }` where `__code_error__` is: -/// -/// - [`spacetimedb_primitives::errno::NO_SUCH_ITER`] -/// when `iter` is not a valid iterator. -/// -/// Throws a `TypeError` if: -/// - `iter` is not a `u32`. -fn row_iter_bsatn_close<'scope>( - scope: &mut PinScope<'scope, '_>, - args: FunctionCallbackArguments<'scope>, -) -> SysCallResult<()> { - let row_iter_idx: u32 = deserialize_js(scope, args.get(0))?; - let row_iter_idx = RowIterIdx(row_iter_idx); - - // Retrieve the iterator by `row_iter_idx`, or error. - let env = get_env(scope)?; - - // Retrieve the iterator by `row_iter_idx`, or error. - if env.iters.take(row_iter_idx).is_none() { - return Err(no_such_iter(scope)); - } else { - // TODO(Centril): consider putting these into a pool for reuse. - } - - Ok(()) + (_, Some(_)) => false, + }; + Ok((done, buffer)) } /// Module ABI that inserts a row into the table identified by `table_id`, @@ -1394,318 +1034,6 @@ fn datastore_delete_all_by_eq_bsatn( Ok(count) } -/// # Signature -/// -/// ```ignore -/// volatile_nonatomic_schedule_immediate(reducer_name: string, args: u8[]) -> undefined -/// ``` -fn volatile_nonatomic_schedule_immediate<'scope>( - scope: &mut PinScope<'scope, '_>, - args: FunctionCallbackArguments<'scope>, -) -> SysCallResult<()> { - let name: String = deserialize_js(scope, args.get(0))?; - let args: Vec = deserialize_js(scope, args.get(1))?; - - get_env(scope)? - .instance_env - .scheduler - .volatile_nonatomic_schedule_immediate(name, crate::host::FunctionArgs::Bsatn(args.into())); - - Ok(()) -} - -/// Module ABI that logs at `level` a `message` message occurring -/// at the parent stack frame. -/// -/// The `message` is interpreted lossily as a UTF-8 string. -/// -/// # Signature -/// -/// ```ignore -/// console_log(level: u8, message: string) -> u32 -/// ``` -/// -/// # Types -/// -/// - `u8` is `number` in JS restricted to unsigned 8-bit integers. -/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. -/// -/// # Returns -/// -/// Returns nothing. -fn console_log<'scope>(scope: &mut PinScope<'scope, '_>, args: FunctionCallbackArguments<'scope>) -> SysCallResult<()> { - let level: u32 = deserialize_js(scope, args.get(0))?; - - let msg = args.get(1).cast::(); - let mut buf = scratch_buf::<128>(); - let msg = msg.to_rust_cow_lossy(scope, &mut buf); - - let frame: Local<'_, v8::StackFrame> = v8::StackTrace::current_stack_trace(scope, 2) - .ok_or_else(exception_already_thrown)? - .get_frame(scope, 1) - .ok_or_else(exception_already_thrown)?; - let mut buf = scratch_buf::<32>(); - let filename = frame - .get_script_name(scope) - .map(|s| s.to_rust_cow_lossy(scope, &mut buf)); - - let level = (level as u8).into(); - let trace = if level == LogLevel::Panic { - JsStackTrace::from_current_stack_trace(scope)? - } else { - <_>::default() - }; - - let env = get_env(scope).inspect_err(|_| { - tracing::warn!( - "{}:{} {msg}", - filename.as_deref().unwrap_or("unknown"), - frame.get_line_number() - ); - })?; - - let function = env.log_record_function(); - let record = Record { - // TODO: figure out whether to use walltime now or logical reducer now (env.reducer_start) - ts: InstanceEnv::now_for_logging(), - target: None, - filename: filename.as_deref(), - line_number: Some(frame.get_line_number() as u32), - function, - message: &msg, - }; - - env.instance_env.console_log(level, &record, &trace); - - Ok(()) -} - -/// Module ABI that begins a timing span with `name`. -/// -/// When the returned `ConsoleTimerId` is passed to [`console_timer_end`], -/// the duration between the calls will be printed to the module's logs. -/// -/// The `name` is interpreted lossily as a UTF-8 string. -/// -/// # Signature -/// -/// ```ignore -/// console_timer_start(name: string) -> u32 -/// ``` -/// -/// # Types -/// -/// - `u8` is `number` in JS restricted to unsigned 8-bit integers. -/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. -/// -/// # Returns -/// -/// Returns a `u32` that is the `ConsoleTimerId`. -/// -/// # Throws -/// -/// Throws a `TypeError` if: -/// - `name` is not a `string`. -fn console_timer_start<'scope>( - scope: &mut PinScope<'scope, '_>, - args: FunctionCallbackArguments<'scope>, -) -> SysCallResult { - let name = args.get(0).cast::(); - let mut buf = scratch_buf::<128>(); - let name = name.to_rust_cow_lossy(scope, &mut buf).into_owned(); - - let span_id = get_env(scope)?.timing_spans.insert(TimingSpan::new(name)).0; - Ok(span_id) -} - -/// Module ABI that ends a timing span with `span_id`. -/// -/// # Signature -/// -/// ```ignore -/// console_timer_end(span_id: u32) -> undefined throws { -/// __code_error__: NO_SUCH_CONSOLE_TIMER -/// } -/// ``` -/// -/// # Types -/// -/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. -/// -/// # Returns -/// -/// Returns nothing. -/// -/// # Throws -/// -/// Throws `{ __code_error__: u16 }` where `__code_error__` is: -/// -/// - [`spacetimedb_primitives::errno::NO_SUCH_CONSOLE_TIMER`] -/// when `span_id` doesn't refer to an active timing span. -/// -/// Throws a `TypeError` if: -/// - `span_id` is not a `u32`. -fn console_timer_end<'scope>( - scope: &mut PinScope<'scope, '_>, - args: FunctionCallbackArguments<'scope>, -) -> SysCallResult<()> { - let span_id: u32 = deserialize_js(scope, args.get(0))?; - - let env = get_env(scope)?; - let Some(span) = env.timing_spans.take(TimingSpanIdx(span_id)) else { - let exc = CodeError::from_code(scope, errno::NO_SUCH_CONSOLE_TIMER.get())?; - return Err(exc.throw(scope).into()); - }; - let function = env.log_record_function(); - env.instance_env.console_timer_end(&span, function); - - Ok(()) -} - -/// Module ABI to read a JWT payload associated with a connection ID from the system tables. -/// -/// # Signature -/// -/// ```ignore -/// get_jwt_payload(connection_id: u128) -> u8[] throws { -/// __code_error__: -/// NOT_IN_TRANSACTION -/// } -/// ``` -/// -/// # Types -/// -/// - `u128` is `bigint` in JS restricted to unsigned 128-bit integers. -/// -/// # Returns -/// -/// Returns a byte array encoding the JWT payload if one is found. If one is not found, an -/// empty byte array is returned. -/// -/// # Throws -/// -/// Throws `{ __code_error__: u16 }` where `__code_error__` is: -/// -/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] -/// when called outside of a transaction. -fn get_jwt_payload(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult> { - let connection_id: u128 = deserialize_js(scope, args.get(0))?; - let connection_id = ConnectionId::from_u128(connection_id); - let payload = get_env(scope)? - .instance_env - .get_jwt_payload(connection_id)? - .map(String::into_bytes) - .unwrap_or_default(); - Ok(payload) -} - -/// Module ABI that returns the module identity. -/// -/// # Signature -/// -/// ```ignore -/// identity() -> { __identity__: u256 } -/// ``` -/// -/// # Types -/// -/// - `u256` is `bigint` in JS restricted to unsigned 256-bit integers. -/// -/// # Returns -/// -/// Returns the module identity. -fn identity<'scope>(scope: &mut PinScope<'scope, '_>, _: FunctionCallbackArguments<'scope>) -> SysCallResult { - Ok(*get_env(scope)?.instance_env.database_identity()) -} - -/// Execute an HTTP request in the context of a procedure. -/// -/// # Signature -/// -/// ```ignore -/// function procedure_http_request( -/// request: Uint8Array, -/// body: Uint8Array | string -/// ): [response: Uint8Array, body: Uint8Array]; -/// ``` -/// -/// Accepts a BSATN-encoded [`spacetimedb_lib::http::Request`] and a request body, and -/// returns a BSATN-encoded [`spacetimedb_lib::http::Response`] and the response body. -fn procedure_http_request<'scope>( - scope: &mut PinScope<'scope, '_>, - args: FunctionCallbackArguments<'scope>, -) -> SysCallResult> { - use spacetimedb_lib::http as st_http; - - let request = - cast!(scope, args.get(0), v8::Uint8Array, "Uint8Array for procedure request").map_err(|e| e.throw(scope))?; - - let request = bsatn::from_slice::(request.get_contents(&mut [])) - .map_err(|e| TypeError(format!("failed to decode http request: {e}")).throw(scope))?; - - let request_body = args.get(1); - let request_body = if let Ok(s) = request_body.try_cast::() { - Bytes::from(s.to_rust_string_lossy(scope)) - } else { - let bytes = cast!( - scope, - request_body, - v8::Uint8Array, - "Uint8Array or string for request body" - ) - .map_err(|e| e.throw(scope))?; - Bytes::copy_from_slice(bytes.get_contents(&mut [])) - }; - - let env = get_env(scope)?; - - let fut = env.instance_env.http_request(request, request_body)?; - - let rt = tokio::runtime::Handle::current(); - let (response, response_body) = rt.block_on(fut)?; - - let response = bsatn::to_vec(&response).expect("failed to serialize `HttpResponse`"); - let response = make_uint8array(scope, response); - - let response_body = match response_body.try_into_mut() { - Ok(bytes_mut) => make_uint8array(scope, Box::new(bytes_mut)), - Err(bytes) => make_uint8array(scope, Vec::from(bytes)), - }; - - Ok(v8::Array::new_with_elements( - scope, - &[response.into(), response_body.into()], - )) -} - -fn procedure_start_mut_tx<'scope>( - scope: &mut PinScope<'scope, '_>, - _args: FunctionCallbackArguments<'_>, -) -> SysCallResult> { - let env = get_env(scope)?; - - env.instance_env.start_mutable_tx()?; - - let timestamp = Timestamp::now().to_micros_since_unix_epoch() as u64; - - Ok(v8::BigInt::new_from_u64(scope, timestamp)) -} - -fn procedure_abort_mut_tx(scope: &mut PinScope<'_, '_>, _args: FunctionCallbackArguments<'_>) -> SysCallResult<()> { - let env = get_env(scope)?; - - env.instance_env.abort_mutable_tx()?; - Ok(()) -} - -fn procedure_commit_mut_tx(scope: &mut PinScope<'_, '_>, _args: FunctionCallbackArguments<'_>) -> SysCallResult<()> { - let env = get_env(scope)?; - - env.instance_env.commit_mutable_tx()?; - - Ok(()) -} - fn datastore_index_scan_point_bsatn( scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>, diff --git a/crates/core/src/host/v8/syscall/v2.rs b/crates/core/src/host/v8/syscall/v2.rs new file mode 100644 index 00000000000..d160dcb2c50 --- /dev/null +++ b/crates/core/src/host/v8/syscall/v2.rs @@ -0,0 +1,1119 @@ +use super::super::de::deserialize_js; +use super::super::error::{ + handle_sys_call_error, no_such_iter, throw_if_terminated, BufferTooSmall, ErrorOrException, ExcResult, + ExceptionThrown, SysCallResult, OOB, +}; +use super::super::from_value::cast; +use super::super::ser::serialize_to_js; +use super::super::string::{str_from_ident, StringConst}; +use super::super::to_value::ToValue; +use super::super::util::{make_dataview, make_uint8array}; +use super::super::{call_free_fun, env_on_isolate, Throwable}; +use super::common::{ + console_log, console_timer_end, console_timer_start, datastore_index_scan_range_bsatn_inner, + datastore_table_row_count, datastore_table_scan_bsatn, deserialize_row_iter_idx, get_env, identity, + index_id_from_name, procedure_abort_mut_tx, procedure_commit_mut_tx, procedure_http_request, + procedure_start_mut_tx, row_iter_bsatn_close, table_id_from_name, volatile_nonatomic_schedule_immediate, +}; +use super::hooks::HookFunctions; +use super::hooks::{get_hook_function, set_hook_slots}; +use super::{AbiVersion, ModuleHookKey}; +use crate::host::instance_env::InstanceEnv; +use crate::host::wasm_common::instrumentation::span; +use crate::host::wasm_common::module_host_actor::{AnonymousViewOp, ReducerOp, ReducerResult, ViewOp, ViewReturnData}; +use crate::host::wasm_common::RowIterIdx; +use crate::host::{AbiCall, ArgsTuple}; +use anyhow::Context; +use bytes::Bytes; +use core::slice; +use spacetimedb_lib::Identity; +use spacetimedb_primitives::{ColId, IndexId, ReducerId, TableId, ViewFnPtr}; +use spacetimedb_sats::u256; +use v8::{ + callback_scope, ArrayBuffer, ConstructorBehavior, DataView, Function, FunctionCallbackArguments, Local, Module, + Object, PinCallbackScope, PinScope, Value, +}; + +macro_rules! create_synthetic_module { + ($scope:expr, $module_name:expr $(, ($wrapper:ident, $abi_call:expr, $fun:ident))* $(,)?) => {{ + let export_names = &[$(str_from_ident!($fun).string($scope)),*]; + let eval_steps = |context, module| { + callback_scope!(unsafe scope, context); + $( + register_module_fun(scope, &module, str_from_ident!($fun), |s, a, rv| { + $wrapper($abi_call, s, a, rv, $fun) + })?; + )* + + Some(v8::undefined(scope).into()) + }; + + Module::create_synthetic_module( + $scope, + const { StringConst::new($module_name) }.string($scope), + export_names, + eval_steps, + ) + }} +} + +/// Registers all module -> host syscalls in the JS module `spacetimedb_sys`. +pub(super) fn sys_v2_0<'scope>(scope: &mut PinScope<'scope, '_>) -> Local<'scope, Module> { + use register_hooks_v2_0 as register_hooks; + create_synthetic_module!( + scope, + "spacetime:sys@2.0", + (with_nothing, (), register_hooks), + (with_sys_result, AbiCall::TableIdFromName, table_id_from_name), + (with_sys_result, AbiCall::IndexIdFromName, index_id_from_name), + ( + with_sys_result, + AbiCall::DatastoreTableRowCount, + datastore_table_row_count + ), + ( + with_sys_result, + AbiCall::DatastoreTableScanBsatn, + datastore_table_scan_bsatn + ), + ( + with_sys_result, + AbiCall::DatastoreIndexScanRangeBsatn, + datastore_index_scan_range_bsatn + ), + (with_sys_result, AbiCall::RowIterBsatnAdvance, row_iter_bsatn_advance), + (with_sys_result, AbiCall::RowIterBsatnClose, row_iter_bsatn_close), + (with_sys_result, AbiCall::DatastoreInsertBsatn, datastore_insert_bsatn), + (with_sys_result, AbiCall::DatastoreUpdateBsatn, datastore_update_bsatn), + ( + with_sys_result, + AbiCall::DatastoreDeleteByIndexScanRangeBsatn, + datastore_delete_by_index_scan_range_bsatn + ), + ( + with_sys_result, + AbiCall::DatastoreDeleteAllByEqBsatn, + datastore_delete_all_by_eq_bsatn + ), + ( + with_sys_result, + AbiCall::VolatileNonatomicScheduleImmediate, + volatile_nonatomic_schedule_immediate + ), + (with_sys_result, AbiCall::ConsoleLog, console_log), + (with_sys_result, AbiCall::ConsoleTimerStart, console_timer_start), + (with_sys_result, AbiCall::ConsoleTimerEnd, console_timer_end), + (with_sys_result, AbiCall::Identity, identity), + (with_sys_result, AbiCall::GetJwt, get_jwt_payload), + (with_sys_result, AbiCall::ProcedureHttpRequest, procedure_http_request), + ( + with_sys_result, + AbiCall::ProcedureStartMutTransaction, + procedure_start_mut_tx + ), + ( + with_sys_result, + AbiCall::ProcedureAbortMutTransaction, + procedure_abort_mut_tx + ), + ( + with_sys_result, + AbiCall::ProcedureCommitMutTransaction, + procedure_commit_mut_tx + ), + ( + with_sys_result, + AbiCall::DatastoreIndexScanPointBsatn, + datastore_index_scan_point_bsatn + ), + ( + with_sys_result, + AbiCall::DatastoreDeleteByIndexScanPointBsatn, + datastore_delete_by_index_scan_point_bsatn + ), + ) +} + +/// Registers a function in `module` +/// where the function has `name` and does `body`. +fn register_module_fun( + scope: &mut PinCallbackScope<'_, '_>, + module: &Local<'_, Module>, + name: &'static StringConst, + body: impl Copy + for<'scope> Fn(&mut PinScope<'scope, '_>, FunctionCallbackArguments<'scope>, v8::ReturnValue<'_>), +) -> Option { + // Convert the name. + let name = name.string(scope); + + // Convert the function. + let fun = Function::builder(adapt_fun(body)).constructor_behavior(ConstructorBehavior::Throw); + let fun = fun.build(scope)?.into(); + + // Set the export on the module. + module.set_synthetic_module_export(scope, name, fun) +} + +/// Adapts `fun` to check for termination +fn adapt_fun( + fun: impl Copy + for<'scope> Fn(&mut PinScope<'scope, '_>, FunctionCallbackArguments<'scope>, v8::ReturnValue<'_>), +) -> impl Copy + for<'scope> Fn(&mut PinScope<'scope, '_>, FunctionCallbackArguments<'scope>, v8::ReturnValue<'_>) { + move |scope, args, rv| { + if throw_if_terminated(scope) { + return; + } + + fun(scope, args, rv) + } +} + +trait JsReturnValue { + fn set_return(self, scope: &mut PinScope<'_, '_>, rv: v8::ReturnValue<'_>); +} + +macro_rules! impl_returnvalue { + ($t:ty, $set:ident) => { + impl_returnvalue!($t, (me, _, rv) => rv.$set(me)); + }; + ($t:ty, self$($field:tt)*) => { + impl_returnvalue!($t, (me, scope, rv) => me$($field)*.set_return(scope, rv)); + }; + ($t:ty, ($me:pat, $scope:pat, $rv:ident) => $body:expr) => { + impl JsReturnValue for $t { + fn set_return(self, $scope: &mut PinScope<'_, '_>, #[allow(unused_mut)] mut $rv: v8::ReturnValue<'_>) { + let $me = self; + $body + } + } + }; +} + +impl_returnvalue!((), ((), _, rv) => rv.set_undefined()); +impl_returnvalue!(u32, set_uint32); +impl_returnvalue!(i32, set_int32); +impl_returnvalue!(u64, (me, scope, rv) => rv.set(me.to_value(scope))); +impl_returnvalue!(u128, (me, scope, rv) => rv.set(me.to_value(scope))); +impl_returnvalue!(u256, (me, scope, rv) => rv.set(me.to_value(scope))); + +impl_returnvalue!(TableId, self.0); +impl_returnvalue!(IndexId, self.0); +impl_returnvalue!(RowIterIdx, self.0); +impl_returnvalue!(Identity, self.to_u256()); + +impl<'s, T> JsReturnValue for Local<'s, T> +where + Self: Into>, +{ + fn set_return(self, _scope: &mut PinScope<'_, '_>, mut rv: v8::ReturnValue<'_>) { + rv.set(self.into()) + } +} + +/// Wraps `run` in [`with_span`] and returns the return value of `run` to JS. +/// Handles [`SysCallError`] if it occurs by throwing exceptions into JS. +fn with_sys_result<'scope, O: JsReturnValue>( + abi_call: AbiCall, + scope: &mut PinScope<'scope, '_>, + args: FunctionCallbackArguments<'scope>, + rv: v8::ReturnValue<'_>, + run: impl FnOnce(&mut PinScope<'scope, '_>, FunctionCallbackArguments<'scope>) -> SysCallResult, +) { + // Start the span. + let span_start = span::CallSpanStart::new(abi_call); + + // Call `fun` with `args` in `scope`. + let result = run(scope, args).map_err(|err| handle_sys_call_error(abi_call, scope, err)); + + // Track the span of this call. + let span = span_start.end(); + if let Some(env) = env_on_isolate(scope) { + span::record_span(&mut env.call_times, span); + } + + if let Ok(ret) = result { + ret.set_return(scope, rv) + } +} + +/// A higher order function conforming to the interface of [`with_sys_result`]. +fn with_nothing<'scope, O: JsReturnValue>( + (): (), + scope: &mut PinScope<'scope, '_>, + args: FunctionCallbackArguments<'scope>, + rv: v8::ReturnValue<'_>, + run: impl FnOnce(&mut PinScope<'scope, '_>, FunctionCallbackArguments<'scope>) -> ExcResult, +) { + if let Ok(ret) = run(scope, args) { + ret.set_return(scope, rv) + } +} + +/// Module ABI that registers the functions called by the host. +/// +/// # Signature +/// +/// ```ignore +/// register_hooks(hooks: { +/// __describe_module__: () => u8[]; +/// __call_reducer__: ( +/// reducer_id: u32, +/// sender: u256, +/// conn_id: u128, +/// timestamp: i64, +/// args_buf: u8[] +/// ) => { tag: 'ok' } | { tag: 'err'; value: string }; +/// }): void +/// ``` +/// +/// # Types +/// +/// - `u8` is `number` in JS restricted to unsigned 8-bit integers. +/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. +/// - `i64` is `bigint` in JS restricted to signed 64-bit integers. +/// - `u128` is `bigint` in JS restricted to unsigned 128-bit integers. +/// - `u256` is `bigint` in JS restricted to unsigned 256-bit integers. +/// +/// # Returns +/// +/// Returns nothing. +/// +/// # Throws +/// +/// Throws a `TypeError` if: +/// - `hooks` is not an object that has functions `__describe_module__` and `__call_reducer__`. +fn register_hooks_v2_0<'scope>(scope: &mut PinScope<'scope, '_>, args: FunctionCallbackArguments<'_>) -> ExcResult<()> { + // Convert `hooks` to an object. + let hooks = cast!(scope, args.get(0), Object, "hooks object").map_err(|e| e.throw(scope))?; + + let describe_module = get_hook_function(scope, hooks, str_from_ident!(__describe_module__))?; + let call_reducer = get_hook_function(scope, hooks, str_from_ident!(__call_reducer__))?; + let call_view = get_hook_function(scope, hooks, str_from_ident!(__call_view__))?; + let call_view_anon = get_hook_function(scope, hooks, str_from_ident!(__call_view_anon__))?; + let call_procedure = get_hook_function(scope, hooks, str_from_ident!(__call_procedure__))?; + + // Set the hooks. + set_hook_slots( + scope, + AbiVersion::V2, + &[ + (ModuleHookKey::DescribeModule, describe_module), + (ModuleHookKey::CallReducer, call_reducer), + (ModuleHookKey::CallView, call_view), + (ModuleHookKey::CallAnonymousView, call_view_anon), + (ModuleHookKey::CallProcedure, call_procedure), + ], + )?; + + Ok(()) +} + +/// Calls the `__call_reducer__` function `fun`. +pub(super) fn call_call_reducer<'scope>( + scope: &mut PinScope<'scope, '_>, + hooks: &HookFunctions<'scope>, + op: ReducerOp<'_>, + reducer_args_buf: Local<'scope, ArrayBuffer>, +) -> ExcResult { + let ReducerOp { + id: ReducerId(reducer_id), + name: _, + caller_identity: sender, + caller_connection_id: conn_id, + timestamp, + args: reducer_args, + } = op; + // Serialize the arguments. + let reducer_id = serialize_to_js(scope, &reducer_id)?; + let sender = serialize_to_js(scope, &sender.to_u256())?; + let conn_id: v8::Local<'_, v8::Value> = serialize_to_js(scope, &conn_id.to_u128())?; + let timestamp = serialize_to_js(scope, ×tamp.to_micros_since_unix_epoch())?; + let reducer_args = reducer_args_to_value(scope, reducer_args, reducer_args_buf); + + let args = &[reducer_id, sender, conn_id, timestamp, reducer_args]; + + // Call the function. + let ret = call_free_fun(scope, hooks.call_reducer, args)?; + + // Deserialize the user result. + let user_res = if ret.is_undefined() { + Ok(()) + } else { + deserialize_js(scope, ret)? + }; + + Ok(user_res) +} + +/// Converts `args` into a `Value`. +fn reducer_args_to_value<'scope>( + scope: &mut PinScope<'scope, '_>, + args: &ArgsTuple, + buffer: Local<'scope, ArrayBuffer>, +) -> Local<'scope, Value> { + let reducer_args = &**args.get_bsatn(); + + let len = reducer_args.len(); + let wrote = with_arraybuffer_mut(buffer, |buf| { + if len > buf.len() { + // Buffer is too small. + return false; + } + let dst = &mut buf[..len]; + dst.copy_from_slice(reducer_args); + true + }); + + let dv = if wrote { + // Fall back to allocating new buffers. + DataView::new(scope, buffer, 0, len) + } else { + // Fall back to allocating new buffers. + make_dataview(scope, >::from(reducer_args)) + }; + + dv.into() +} + +/// Calls the `__call_view__` function `fun`. +pub(super) fn call_call_view( + scope: &mut PinScope<'_, '_>, + hooks: &HookFunctions<'_>, + op: ViewOp<'_>, +) -> Result> { + let fun = hooks.call_view.context("`__call_view__` was never defined")?; + + let ViewOp { + fn_ptr: ViewFnPtr(view_id), + view_id: _, + table_id: _, + name: _, + sender, + timestamp: _, + args: view_args, + } = op; + // Serialize the arguments. + let view_id = serialize_to_js(scope, &view_id)?; + let sender = serialize_to_js(scope, &sender.to_u256())?; + let view_args = serialize_to_js(scope, view_args.get_bsatn())?; + let args = &[view_id, sender, view_args]; + + // Call the function. + let ret = call_free_fun(scope, fun, args)?; + + // Returns an object with a `data` field containing the bytes. + let ret = cast!(scope, ret, v8::Object, "object return from `__call_view_anon__`").map_err(|e| e.throw(scope))?; + + let Some(data_key) = v8::String::new(scope, "data") else { + return Err(ErrorOrException::Err(anyhow::anyhow!("error creating a v8 string"))); + }; + let Some(data_val) = ret.get(scope, data_key.into()) else { + return Err(ErrorOrException::Err(anyhow::anyhow!( + "data key not found in return object" + ))); + }; + + let ret = cast!( + scope, + data_val, + v8::Uint8Array, + "bytes in the `data` field returned from `__call_view_anon__`" + ) + .map_err(|e| e.throw(scope))?; + let bytes = ret.get_contents(&mut []); + + Ok(ViewReturnData::HeaderFirst(Bytes::copy_from_slice(bytes))) +} + +/// Calls the `__call_view_anon__` function `fun`. +pub(super) fn call_call_view_anon( + scope: &mut PinScope<'_, '_>, + hooks: &HookFunctions<'_>, + op: AnonymousViewOp<'_>, +) -> Result> { + let fun = hooks.call_view_anon.context("`__call_view_anon__` was never defined")?; + + let AnonymousViewOp { + fn_ptr: ViewFnPtr(view_id), + view_id: _, + table_id: _, + name: _, + timestamp: _, + args: view_args, + } = op; + // Serialize the arguments. + let view_id = serialize_to_js(scope, &view_id)?; + let view_args = serialize_to_js(scope, view_args.get_bsatn())?; + let args = &[view_id, view_args]; + + // Call the function. + let ret = call_free_fun(scope, fun, args)?; + + let ret = cast!(scope, ret, v8::Object, "object return from `__call_view_anon__`").map_err(|e| e.throw(scope))?; + + let Some(data_key) = v8::String::new(scope, "data") else { + return Err(ErrorOrException::Err(anyhow::anyhow!("error creating a v8 string"))); + }; + let Some(data_val) = ret.get(scope, data_key.into()) else { + return Err(ErrorOrException::Err(anyhow::anyhow!( + "data key not found in return object" + ))); + }; + + let ret = cast!( + scope, + data_val, + v8::Uint8Array, + "bytes in the `data` field returned from `__call_view_anon__`" + ) + .map_err(|e| e.throw(scope))?; + let bytes = ret.get_contents(&mut []); + + Ok(ViewReturnData::HeaderFirst(Bytes::copy_from_slice(bytes))) +} + +/// Module ABI that finds all rows in the index identified by `index_id`, +/// according to `prefix`, `rstart`, and `rend` where: +/// - `prefix = buffer[...prefix_len]` +/// - `rstart = buffer[prefix_len..prefix_len + rstart_len]` +/// - `rend = buffer[prefix_len + rstart_len..prefix_len + rstart_len + rend_len]` +/// if `rend_len > 0` +/// - `rend = rstart` if `rend_len == 0` +/// +/// The index itself has a schema/type. +/// The `prefix` is decoded to the initial `prefix_elems` `AlgebraicType`s +/// whereas `rstart` and `rend` are decoded to the `prefix_elems + 1` `AlgebraicType` +/// where the `AlgebraicValue`s are wrapped in `Bound`. +/// That is, `rstart, rend` are BSATN-encoded `Bound`s. +/// +/// Matching is then defined by equating `prefix` +/// to the initial `prefix_elems` columns of the index +/// and then imposing `rstart` as the starting bound +/// and `rend` as the ending bound on the `prefix_elems + 1` column of the index. +/// Remaining columns of the index are then unbounded. +/// Note that the `prefix` in this case can be empty (`prefix_elems = 0`), +/// in which case this becomes a ranged index scan on a single-col index +/// or even a full table scan if `rstart` and `rend` are both unbounded. +/// +/// The relevant table for the index is found implicitly via the `index_id`, +/// which is unique for the module. +/// +/// On success, the iterator handle is written to the `out` pointer. +/// This handle can be advanced by [`row_iter_bsatn_advance`]. +/// +/// # Non-obvious queries +/// +/// For an index on columns `[a, b, c]`: +/// +/// - `a = x, b = y` is encoded as a prefix `[x, y]` +/// and a range `Range::Unbounded`, +/// or as a prefix `[x]` and a range `rstart = rend = Range::Inclusive(y)`. +/// - `a = x, b = y, c = z` is encoded as a prefix `[x, y]` +/// and a range `rstart = rend = Range::Inclusive(z)`. +/// - A sorted full scan is encoded as an empty prefix +/// and a range `Range::Unbounded`. +/// +/// # Signature +/// +/// ```ignore +/// datastore_index_scan_range_bsatn( +/// index_id: u32, +/// buffer: ArrayBuffer, +/// prefix_len: u32, +/// prefix_elems: u16, +/// rstart_len: u32, +/// rend_len: u32, +/// ) -> u32 throws { +/// __code_error__: NOT_IN_TRANSACTION | NO_SUCH_INDEX | BSATN_DECODE_ERROR +/// } +/// ``` +/// +/// # Types +/// +/// - `u8` is `number` in JS restricted to unsigned 8-bit integers. +/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// - `u64` is `bigint` in JS restricted to unsigned 64-bit integers. +/// +/// # Returns +/// +/// Returns a `u32` that is the iterator handle. +/// This handle can be advanced by [`row_iter_bsatn_advance`]. +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] +/// when called outside of a transaction. +/// +/// - [`spacetimedb_primitives::errno::NO_SUCH_INDEX`] +/// when `index_id` is not a known ID of an index. +/// +/// - [`spacetimedb_primitives::errno::BSATN_DECODE_ERROR`] +/// when `prefix` cannot be decoded to +/// a `prefix_elems` number of `AlgebraicValue` +/// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. +/// Or when `rstart` or `rend` cannot be decoded to an `Bound` +/// where the inner `AlgebraicValue`s are +/// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. +/// +/// Throws a `TypeError` if: +/// - `index_id` is not a `u32`. +/// - `prefix_len`, `rstart_len`, and `rend_len` are not `u32`s. +/// - `prefix_elems` is not a `u16`. +/// +/// Throws a `RangeError` if any of these are out of bounds of `buffer`: +/// - `prefix_len`, +/// - `prefix_len + rstart_len`, +/// - or `prefix_len + rstart_len + rend_len` +fn datastore_index_scan_range_bsatn( + scope: &mut PinScope<'_, '_>, + args: FunctionCallbackArguments<'_>, +) -> SysCallResult { + let index_id: IndexId = deserialize_js(scope, args.get(0))?; + let buf = cast!(scope, args.get(1), v8::ArrayBuffer, "`ArrayBuffer`").map_err(|e| e.throw(scope))?; + let prefix_len = deserialize_js::(scope, args.get(2))? as usize; + let prefix_elems: ColId = deserialize_js(scope, args.get(3))?; + let rstart_len = deserialize_js::(scope, args.get(4))? as usize; + let rend_len = deserialize_js::(scope, args.get(5))? as usize; + + with_arraybuffer(buf, |mut buf| { + let prefix = buf.split_off(..prefix_len).ok_or(OOB)?; + let rstart = buf.split_off(..rstart_len).ok_or(OOB)?; + let rend = if rend_len == 0 { + rstart + } else { + buf.split_off(..rend_len).ok_or(OOB)? + }; + + datastore_index_scan_range_bsatn_inner(scope, index_id, prefix, prefix_elems, rstart, rend) + }) +} + +/// Module ABI that reads rows from the given iterator registered under `iter`. +/// +/// Takes rows from the iterator with id `iter` +/// and returns them encoded in the BSATN format. +/// +/// The rows returned take up at most `buffer_max_len` bytes. +/// A row is never broken up between calls. +/// +/// Aside from the BSATN, +/// the function also returns `true` when the iterator been exhausted +/// and there are no more rows to read. +/// This leads to the iterator being immediately destroyed. +/// Conversely, `false` is returned if there are more rows to read. +/// Note that the host is free to reuse allocations in a pool, +/// destroying the handle logically does not entail that memory is necessarily reclaimed. +/// +/// # Signature +/// +/// ```ignore +/// row_iter_bsatn_advance(iter: u32, buffer_max_len: u32) -> (boolean, u8[]) throws +/// { __code_error__: NO_SUCH_ITER } | { __buffer_too_small__: number } +/// ``` +/// +/// # Types +/// +/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// +/// # Returns +/// +/// Returns `(exhausted: boolean, rows_bsatn: u8[])` where: +/// - `exhausted` is `true` if there are no more rows to read, +/// - `rows_bsatn` are the BSATN-encoded row bytes, concatenated. +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NO_SUCH_ITER`] +/// when `iter` is not a valid iterator. +/// +/// Throws `{ __buffer_too_small__: number }` +/// when there are rows left but they cannot fit in `buffer`. +/// When this occurs, `__buffer_too_small__` contains the size of the next item in the iterator. +/// To make progress, the caller should call `row_iter_bsatn_advance` +/// with `buffer_max_len >= __buffer_too_small__` and try again. +/// +/// Throws a `TypeError` if: +/// - `iter` and `buffer_max_len` are not `u32`s. +fn row_iter_bsatn_advance<'scope>( + scope: &mut PinScope<'scope, '_>, + args: FunctionCallbackArguments<'scope>, +) -> SysCallResult { + let row_iter_idx = deserialize_row_iter_idx(scope, args.get(0))?; + let array_buffer = cast!(scope, args.get(1), v8::ArrayBuffer, "`ArrayBuffer`").map_err(|e| e.throw(scope))?; + + // Retrieve the iterator by `row_iter_idx`, or error. + let env = get_env(scope)?; + let Some(iter) = env.iters.get_mut(row_iter_idx) else { + return Err(no_such_iter(scope)); + }; + + // Fill the buffer as much as possible. + let written = with_arraybuffer_mut(array_buffer, |buf| { + InstanceEnv::fill_buffer_from_iter(iter, buf, &mut env.chunk_pool) + }); + + let next_buf_len = iter.as_slice().first().map(|v| v.len()); + let done = match (written, next_buf_len) { + // Nothing was written and the iterator is not exhausted. + (0, Some(min_len)) => { + let min_len = min_len.try_into().unwrap(); + let exc = BufferTooSmall::from_requirement(scope, min_len)?; + return Err(exc.throw(scope).into()); + } + // The iterator is exhausted, destroy it, and tell the caller. + (_, None) => { + env.iters.take(row_iter_idx); + true + } + // Something was written, but the iterator is not exhausted. + (_, Some(_)) => false, + }; + + let written: i32 = written.try_into().unwrap(); + let out = if done { -written } else { written }; + Ok(out) +} + +/// Module ABI that inserts a row into the table identified by `table_id`, +/// where the `row` is an array of bytes. +/// +/// The byte array `row` must be a BSATN-encoded `ProductValue` +/// typed at the table's `ProductType` row-schema. +/// +/// To handle auto-incrementing columns, +/// when the call is successful, +/// the an array of bytes is returned, containing the generated sequence values. +/// These values are written as a BSATN-encoded `pv: ProductValue`. +/// Each `v: AlgebraicValue` in `pv` is typed at the sequence's column type. +/// The `v`s in `pv` are ordered by the order of the columns, in the schema of the table. +/// When the table has no sequences, +/// this implies that the `pv`, and thus `row`, will be empty. +/// +/// # Signature +/// +/// ```ignore +/// datastore_insert_bsatn(table_id: u32, row: u8[]) -> u8[] throws { +/// __code_error__: +/// NOT_IN_TRANSACTION +/// | NOT_SUCH_TABLE +/// | BSATN_DECODE_ERROR +/// | UNIQUE_ALREADY_EXISTS +/// | SCHEDULE_AT_DELAY_TOO_LONG +/// } +/// ``` +/// +/// # Types +/// +/// - `u8` is `number` in JS restricted to unsigned 8-bit integers. +/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// +/// # Returns +/// +/// Returns the generated sequence values encoded in BSATN (see above). +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] +/// when called outside of a transaction. +/// - [`spacetimedb_primitives::errno::NOT_SUCH_TABLE`] +/// when `table_id` is not a known ID of a table. +/// - [`spacetimedb_primitives::errno::`BSATN_DECODE_ERROR`] +/// when `row` cannot be decoded to a `ProductValue`. +/// typed at the `ProductType` the table's schema specifies. +/// - [`spacetimedb_primitives::errno::`UNIQUE_ALREADY_EXISTS`] +/// when inserting `row` would violate a unique constraint. +/// - [`spacetimedb_primitives::errno::`SCHEDULE_AT_DELAY_TOO_LONG`] +/// when the delay specified in the row was too long. +/// +/// Throws a `TypeError` if: +/// - `table_id` is not a `u32`. +/// - `row` is not an array of `u8`s. +fn datastore_insert_bsatn<'scope>( + scope: &mut PinScope<'scope, '_>, + args: FunctionCallbackArguments<'_>, +) -> SysCallResult { + let table_id: TableId = deserialize_js(scope, args.get(0))?; + let row_arr = cast!(scope, args.get(1), v8::ArrayBuffer, "buffer").map_err(|e| e.throw(scope))?; + let row_len = deserialize_js::(scope, args.get(2))? as usize; + + // let row_offset = row_arr.byte_offset(); + let row_len = with_arraybuffer_mut(row_arr, |buf| -> SysCallResult<_> { + let buf = buf.get_mut(..row_len).ok_or(OOB)?; + // Insert the row into the DB and write back the generated column values. + Ok(get_env(scope)?.instance_env.insert(table_id, buf)?) + })?; + + Ok(row_len as u32) +} + +fn with_arraybuffer(buf: Local<'_, v8::ArrayBuffer>, f: impl FnOnce(&[u8]) -> R) -> R { + let buf: &[u8] = match buf.data().map(|p| p.cast::()) { + Some(data) => { + let ptr = data.as_ptr(); + let len = buf.byte_length(); + + // SAFETY: We know `ptr` to be: + // - trivially properly aligned due to `u8`s alignment being 1. + // - non-null as it was derived from `NonNull`. + // - the range is within the allocation of `buffer` + // as `len = buffer.byte_length()`, + // so `buffer` is dereferenceable. + // - `ptr` will point to a valid `[u8]` as it was zero-initialized. + // - nothing is aliasing the pointer. + unsafe { slice::from_raw_parts(ptr, len) } + } + None => &[], + }; + f(buf) +} + +fn with_arraybuffer_mut(buf: Local<'_, v8::ArrayBuffer>, f: impl FnOnce(&mut [u8]) -> R) -> R { + let buf: &mut [u8] = match buf.data().map(|p| p.cast::()) { + // SAFETY: see comment in `with_uint8array_mut` + Some(data) => { + let ptr = data.as_ptr(); + let len = buf.byte_length(); + + // SAFETY: We know `ptr` to be: + // - trivially properly aligned due to `u8`s alignment being 1. + // - non-null as it was derived from `NonNull`. + // - the range is within the allocation of `buffer` + // as `len = buffer.byte_length()`, + // so `buffer` is dereferenceable. + // - `ptr` will point to a valid `[u8]` as it was zero-initialized. + // - nothing is aliasing the pointer. + unsafe { slice::from_raw_parts_mut(ptr, len) } + } + None => &mut [], + }; + f(buf) +} + +/// Module ABI that updates a row into the table identified by `table_id`, +/// where the `row` is an array of bytes. +/// +/// The byte array `row` must be a BSATN-encoded `ProductValue` +/// typed at the table's `ProductType` row-schema. +/// +/// The row to update is found by projecting `row` +/// to the type of the *unique* index identified by `index_id`. +/// If no row is found, the error `NO_SUCH_ROW` is returned. +/// +/// To handle auto-incrementing columns, +/// when the call is successful, +/// the `row` is written back to with the generated sequence values. +/// These values are written as a BSATN-encoded `pv: ProductValue`. +/// Each `v: AlgebraicValue` in `pv` is typed at the sequence's column type. +/// The `v`s in `pv` are ordered by the order of the columns, in the schema of the table. +/// When the table has no sequences, +/// this implies that the `pv`, and thus `row`, will be empty. +/// +/// # Signature +/// +/// ```ignore +/// datastore_update_bsatn(table_id: u32, index_id: u32, row: u8[]) -> u8[] throws { +/// __code_error__: +/// NOT_IN_TRANSACTION +/// | NOT_SUCH_TABLE +/// | NO_SUCH_INDEX +/// | INDEX_NOT_UNIQUE +/// | BSATN_DECODE_ERROR +/// | NO_SUCH_ROW +/// | UNIQUE_ALREADY_EXISTS +/// | SCHEDULE_AT_DELAY_TOO_LONG +/// } +/// ``` +/// +/// # Types +/// +/// - `u8` is `number` in JS restricted to unsigned 8-bit integers. +/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// +/// # Returns +/// +/// Returns the generated sequence values encoded in BSATN (see above). +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] +/// when called outside of a transaction. +/// - [`spacetimedb_primitives::errno::NOT_SUCH_TABLE`] +/// when `table_id` is not a known ID of a table. +/// - [`spacetimedb_primitives::errno::NO_SUCH_INDEX`] +/// when `index_id` is not a known ID of an index. +/// - [`spacetimedb_primitives::errno::INDEX_NOT_UNIQUE`] +/// when the index was not unique. +/// - [`spacetimedb_primitives::errno::`BSATN_DECODE_ERROR`] +/// when `row` cannot be decoded to a `ProductValue`. +/// typed at the `ProductType` the table's schema specifies +/// or when it cannot be projected to the index identified by `index_id`. +/// - [`spacetimedb_primitives::errno::`NO_SUCH_ROW`] +/// when the row was not found in the unique index. +/// - [`spacetimedb_primitives::errno::`UNIQUE_ALREADY_EXISTS`] +/// when inserting `row` would violate a unique constraint. +/// - [`spacetimedb_primitives::errno::`SCHEDULE_AT_DELAY_TOO_LONG`] +/// when the delay specified in the row was too long. +/// +/// Throws a `TypeError` if: +/// - `table_id` is not a `u32`. +/// - `row` is not an array of `u8`s. +fn datastore_update_bsatn<'scope>( + scope: &mut PinScope<'scope, '_>, + args: FunctionCallbackArguments<'_>, +) -> SysCallResult { + let table_id: TableId = deserialize_js(scope, args.get(0))?; + let index_id: IndexId = deserialize_js(scope, args.get(1))?; + let row_arr = cast!(scope, args.get(2), v8::ArrayBuffer, "buffer").map_err(|e| e.throw(scope))?; + let row_len = deserialize_js::(scope, args.get(3))? as usize; + + // Insert the row into the DB and write back the generated column values. + let row_len = with_arraybuffer_mut(row_arr, |buf| -> SysCallResult<_> { + let buf = buf.get_mut(..row_len).ok_or(OOB)?; + // Insert the row into the DB and write back the generated column values. + Ok(get_env(scope)?.instance_env.update(table_id, index_id, buf)?) + })?; + + Ok(row_len as u32) +} + +/// Module ABI that deletes all rows found in the index identified by `index_id`, +/// according to `prefix`, `rstart`, and `rend` where: +/// - `prefix = buffer[...prefix_len]` +/// - `rstart = buffer[prefix_len..prefix_len + rstart_len]` +/// - `rend = buffer[prefix_len + rstart_len..prefix_len + rstart_len + rend_len]` +/// if `rend_len > 0` +/// - `rend = rstart` if `rend_len == 0` +/// +/// This syscall will delete all the rows found by +/// [`datastore_index_scan_range_bsatn`] with the same arguments passed, +/// including `prefix_elems`. +/// See `datastore_index_scan_range_bsatn` for details. +/// +/// # Signature +/// +/// ```ignore +/// datastore_index_scan_range_bsatn( +/// index_id: u32, +/// prefix: u8[], +/// prefix_elems: u16, +/// rstart: u8[], +/// rend: u8[], +/// ) -> u32 throws { +/// __code_error__: NOT_IN_TRANSACTION | NO_SUCH_INDEX | BSATN_DECODE_ERROR +/// } +/// ``` +/// +/// # Types +/// +/// - `u8` is `number` in JS restricted to unsigned 8-bit integers. +/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// +/// # Returns +/// +/// Returns a `u32` that is the number of rows deleted. +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] +/// when called outside of a transaction. +/// +/// - [`spacetimedb_primitives::errno::NO_SUCH_INDEX`] +/// when `index_id` is not a known ID of an index. +/// +/// - [`spacetimedb_primitives::errno::BSATN_DECODE_ERROR`] +/// when `prefix` cannot be decoded to +/// a `prefix_elems` number of `AlgebraicValue` +/// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. +/// Or when `rstart` or `rend` cannot be decoded to an `Bound` +/// where the inner `AlgebraicValue`s are +/// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. +/// +/// Throws a `TypeError` if: +/// - `index_id` is not a `u32`. +/// - `prefix_len`, `rstart_len`, and `rend_len` are not `u32`s. +/// - `prefix_elems` is not a `u16`. +/// +/// Throws a `RangeError` if any of these are out of bounds of `buffer`: +/// - `prefix_len`, +/// - `prefix_len + rstart_len`, +/// - or `prefix_len + rstart_len + rend_len` +fn datastore_delete_by_index_scan_range_bsatn( + scope: &mut PinScope<'_, '_>, + args: FunctionCallbackArguments<'_>, +) -> SysCallResult { + let index_id: IndexId = deserialize_js(scope, args.get(0))?; + let buf = cast!(scope, args.get(1), v8::ArrayBuffer, "`ArrayBuffer`").map_err(|e| e.throw(scope))?; + let prefix_len = deserialize_js::(scope, args.get(2))? as usize; + let prefix_elems: ColId = deserialize_js(scope, args.get(3))?; + let rstart_len = deserialize_js::(scope, args.get(4))? as usize; + let rend_len = deserialize_js::(scope, args.get(5))? as usize; + + with_arraybuffer(buf, |mut buf| { + let oob = || OOB; + + let prefix = buf.split_off(..prefix_len).ok_or_else(oob)?; + let rstart = buf.split_off(..rstart_len).ok_or_else(oob)?; + let rend = if rend_len == 0 { + rstart + } else { + buf.split_off(..rend_len).ok_or_else(oob)? + }; + + // Delete the relevant rows. + let count = get_env(scope)? + .instance_env + .datastore_delete_by_index_scan_range_bsatn(index_id, prefix, prefix_elems, rstart, rend)?; + Ok(count) + }) +} + +/// Module ABI that deletes those rows, in the table identified by `table_id`, +/// that match any row in `relation`. +/// +/// Matching is defined by first BSATN-decoding +/// the array of bytes `relation` to a `Vec` +/// according to the row schema of the table +/// and then using `Ord for AlgebraicValue`. +/// A match happens when `Ordering::Equal` is returned from `fn cmp`. +/// This occurs exactly when the row's BSATN-encoding is equal to the encoding of the `ProductValue`. +/// +/// # Signature +/// +/// ```ignore +/// datastore_delete_all_by_eq_bsatn(table_id: u32, relation: u8[]) -> u32 throws { +/// __code_error__: NOT_IN_TRANSACTION | NO_SUCH_INDEX | BSATN_DECODE_ERROR +/// } +/// ``` +/// +/// # Types +/// +/// - `u8` is `number` in JS restricted to unsigned 8-bit integers. +/// - `u16` is `number` in JS restricted to unsigned 16-bit integers. +/// - `u32` is `number` in JS restricted to unsigned 32-bit integers. +/// +/// # Returns +/// +/// Returns a `u32` that is the number of rows deleted. +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] +/// when called outside of a transaction. +/// +/// - [`spacetimedb_primitives::errno::NO_SUCH_TABLE`] +/// when `table_id` is not a known ID of a table. +/// +/// - [`spacetimedb_primitives::errno::BSATN_DECODE_ERROR`] +/// when `relation` cannot be decoded to `Vec` +/// where each `ProductValue` is typed at the `ProductType` the table's schema specifies. +/// +/// Throws a `TypeError` if: +/// - `table_id` is not a `u32`. +/// - `relation` is not an array of `u8`s. +fn datastore_delete_all_by_eq_bsatn( + scope: &mut PinScope<'_, '_>, + args: FunctionCallbackArguments<'_>, +) -> SysCallResult { + let table_id: TableId = deserialize_js(scope, args.get(0))?; + let buf = cast!(scope, args.get(1), v8::ArrayBuffer, "`ArrayBuffer`").map_err(|e| e.throw(scope))?; + let relation_len = deserialize_js::(scope, args.get(2))? as usize; + + with_arraybuffer(buf, |buf| { + let relation = buf.get(..relation_len).ok_or(OOB)?; + let count = get_env(scope)? + .instance_env + .datastore_delete_all_by_eq_bsatn(table_id, relation)?; + Ok(count) + }) +} + +/// Module ABI to read a JWT payload associated with a connection ID from the system tables. +/// +/// # Signature +/// +/// ```ignore +/// get_jwt_payload(connection_id: u128) -> u8[] throws { +/// __code_error__: +/// NOT_IN_TRANSACTION +/// } +/// ``` +/// +/// # Types +/// +/// - `u128` is `bigint` in JS restricted to unsigned 128-bit integers. +/// +/// # Returns +/// +/// Returns a byte array encoding the JWT payload if one is found. If one is not found, an +/// empty byte array is returned. +/// +/// # Throws +/// +/// Throws `{ __code_error__: u16 }` where `__code_error__` is: +/// +/// - [`spacetimedb_primitives::errno::NOT_IN_TRANSACTION`] +/// when called outside of a transaction. +fn get_jwt_payload<'scope>( + scope: &mut PinScope<'scope, '_>, + args: FunctionCallbackArguments<'_>, +) -> SysCallResult> { + let payload = super::common::get_jwt_payload(scope, args)?; + Ok(make_uint8array(scope, payload)) +} + +fn datastore_index_scan_point_bsatn( + scope: &mut PinScope<'_, '_>, + args: FunctionCallbackArguments<'_>, +) -> SysCallResult { + let index_id: IndexId = deserialize_js(scope, args.get(0))?; + let buf = cast!(scope, args.get(1), v8::ArrayBuffer, "`ArrayBuffer`").map_err(|e| e.throw(scope))?; + let point_len = deserialize_js::(scope, args.get(2))? as usize; + + with_arraybuffer(buf, |buf| { + let point = buf.get(..point_len).ok_or(OOB)?; + + let env = get_env(scope)?; + + // Find the relevant rows. + let chunks = env + .instance_env + .datastore_index_scan_point_bsatn_chunks(&mut env.chunk_pool, index_id, point)?; + + // Insert the encoded + concatenated rows into a new buffer and return its id. + Ok(env.iters.insert(chunks.into_iter()).0) + }) +} + +fn datastore_delete_by_index_scan_point_bsatn( + scope: &mut PinScope<'_, '_>, + args: FunctionCallbackArguments<'_>, +) -> SysCallResult { + let index_id: IndexId = deserialize_js(scope, args.get(0))?; + let buf = cast!(scope, args.get(1), v8::ArrayBuffer, "`ArrayBuffer`").map_err(|e| e.throw(scope))?; + let point_len = deserialize_js::(scope, args.get(2))? as usize; + + with_arraybuffer(buf, |buf| { + let point = buf.get(..point_len).ok_or(OOB)?; + + // Delete the relevant rows. + let count = get_env(scope)? + .instance_env + .datastore_delete_by_index_scan_point_bsatn(index_id, point)?; + Ok(count) + }) +} diff --git a/crates/core/src/host/v8/util.rs b/crates/core/src/host/v8/util.rs index 4fb418189f5..2b923c33f03 100644 --- a/crates/core/src/host/v8/util.rs +++ b/crates/core/src/host/v8/util.rs @@ -30,3 +30,14 @@ pub(super) fn make_uint8array<'scope>( let buf = v8::ArrayBuffer::with_backing_store(scope, &store.make_shared()); v8::Uint8Array::new(scope, buf, 0, len).expect("len > 8 pebibytes") } + +/// Taking a scope and a buffer, return a `v8::Local<'scope, v8::DataView>`. +pub(super) fn make_dataview<'scope>( + scope: &v8::PinScope<'scope, '_>, + buf: impl IntoArrayBufferBackingStore, +) -> v8::Local<'scope, v8::DataView> { + let store = buf.into_backing_store(); + let len = store.byte_length(); + let buf = v8::ArrayBuffer::with_backing_store(scope, &store.make_shared()); + v8::DataView::new(scope, buf, 0, len) +}