diff --git a/crates/bindings-typescript/src/lib/autogen/raw_misc_module_export_v_9_type.ts b/crates/bindings-typescript/src/lib/autogen/raw_misc_module_export_v_9_type.ts index f8a0566672b..e10a92d8844 100644 --- a/crates/bindings-typescript/src/lib/autogen/raw_misc_module_export_v_9_type.ts +++ b/crates/bindings-typescript/src/lib/autogen/raw_misc_module_export_v_9_type.ts @@ -19,12 +19,20 @@ import { import { RawColumnDefaultValueV9 } from './raw_column_default_value_v_9_type'; // Mark import as potentially unused declare type __keep_RawColumnDefaultValueV9 = RawColumnDefaultValueV9; +import { RawProcedureDefV9 } from './raw_procedure_def_v_9_type'; +// Mark import as potentially unused +declare type __keep_RawProcedureDefV9 = RawProcedureDefV9; +import { RawViewDefV9 } from './raw_view_def_v_9_type'; +// Mark import as potentially unused +declare type __keep_RawViewDefV9 = RawViewDefV9; import * as RawMiscModuleExportV9Variants from './raw_misc_module_export_v_9_variants'; // The tagged union or sum type for the algebraic type `RawMiscModuleExportV9`. export type RawMiscModuleExportV9 = - RawMiscModuleExportV9Variants.ColumnDefaultValue; + | RawMiscModuleExportV9Variants.ColumnDefaultValue + | RawMiscModuleExportV9Variants.Procedure + | RawMiscModuleExportV9Variants.View; let _cached_RawMiscModuleExportV9_type_value: __AlgebraicTypeType | null = null; @@ -42,6 +50,13 @@ export const RawMiscModuleExportV9 = { tag: 'ColumnDefaultValue', value, }), + Procedure: ( + value: RawProcedureDefV9 + ): RawMiscModuleExportV9Variants.Procedure => ({ tag: 'Procedure', value }), + View: (value: RawViewDefV9): RawMiscModuleExportV9Variants.View => ({ + tag: 'View', + value, + }), getTypeScriptAlgebraicType(): __AlgebraicTypeType { if (_cached_RawMiscModuleExportV9_type_value) @@ -49,10 +64,17 @@ export const RawMiscModuleExportV9 = { _cached_RawMiscModuleExportV9_type_value = __AlgebraicTypeValue.Sum({ variants: [], }); - _cached_RawMiscModuleExportV9_type_value.value.variants.push({ - name: 'ColumnDefaultValue', - algebraicType: RawColumnDefaultValueV9.getTypeScriptAlgebraicType(), - }); + _cached_RawMiscModuleExportV9_type_value.value.variants.push( + { + name: 'ColumnDefaultValue', + algebraicType: RawColumnDefaultValueV9.getTypeScriptAlgebraicType(), + }, + { + name: 'Procedure', + algebraicType: RawProcedureDefV9.getTypeScriptAlgebraicType(), + }, + { name: 'View', algebraicType: RawViewDefV9.getTypeScriptAlgebraicType() } + ); return _cached_RawMiscModuleExportV9_type_value; }, diff --git a/crates/bindings-typescript/src/lib/autogen/raw_misc_module_export_v_9_variants.ts b/crates/bindings-typescript/src/lib/autogen/raw_misc_module_export_v_9_variants.ts index e65385fac01..a100dd6dc1c 100644 --- a/crates/bindings-typescript/src/lib/autogen/raw_misc_module_export_v_9_variants.ts +++ b/crates/bindings-typescript/src/lib/autogen/raw_misc_module_export_v_9_variants.ts @@ -19,8 +19,16 @@ import { import { RawColumnDefaultValueV9 as RawColumnDefaultValueV9Type } from './raw_column_default_value_v_9_type'; // Mark import as potentially unused declare type __keep_RawColumnDefaultValueV9Type = RawColumnDefaultValueV9Type; +import { RawProcedureDefV9 as RawProcedureDefV9Type } from './raw_procedure_def_v_9_type'; +// Mark import as potentially unused +declare type __keep_RawProcedureDefV9Type = RawProcedureDefV9Type; +import { RawViewDefV9 as RawViewDefV9Type } from './raw_view_def_v_9_type'; +// Mark import as potentially unused +declare type __keep_RawViewDefV9Type = RawViewDefV9Type; export type ColumnDefaultValue = { tag: 'ColumnDefaultValue'; value: RawColumnDefaultValueV9Type; }; +export type Procedure = { tag: 'Procedure'; value: RawProcedureDefV9Type }; +export type View = { tag: 'View'; value: RawViewDefV9Type }; diff --git a/crates/bindings-typescript/src/lib/autogen/raw_procedure_def_v_9_type.ts b/crates/bindings-typescript/src/lib/autogen/raw_procedure_def_v_9_type.ts new file mode 100644 index 00000000000..8c422c869be --- /dev/null +++ b/crates/bindings-typescript/src/lib/autogen/raw_procedure_def_v_9_type.ts @@ -0,0 +1,77 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + AlgebraicType as __AlgebraicTypeValue, + BinaryReader as __BinaryReader, + BinaryWriter as __BinaryWriter, + ConnectionId as __ConnectionId, + Identity as __Identity, + TimeDuration as __TimeDuration, + Timestamp as __Timestamp, + deepEqual as __deepEqual, + type AlgebraicType as __AlgebraicTypeType, + type AlgebraicTypeVariants as __AlgebraicTypeVariants, + type TableHandle as __TableHandle, +} from '../../index'; +import { AlgebraicType } from './algebraic_type_type'; +// Mark import as potentially unused +declare type __keep_AlgebraicType = AlgebraicType; +import { ProductType } from './product_type_type'; +// Mark import as potentially unused +declare type __keep_ProductType = ProductType; + +export type RawProcedureDefV9 = { + name: string; + params: ProductType; + returnType: AlgebraicType; +}; +let _cached_RawProcedureDefV9_type_value: __AlgebraicTypeType | null = null; + +/** + * An object for generated helper functions. + */ +export const RawProcedureDefV9 = { + /** + * A function which returns this type represented as an AlgebraicType. + * This function is derived from the AlgebraicType used to generate this type. + */ + getTypeScriptAlgebraicType(): __AlgebraicTypeType { + if (_cached_RawProcedureDefV9_type_value) + return _cached_RawProcedureDefV9_type_value; + _cached_RawProcedureDefV9_type_value = __AlgebraicTypeValue.Product({ + elements: [], + }); + _cached_RawProcedureDefV9_type_value.value.elements.push( + { name: 'name', algebraicType: __AlgebraicTypeValue.String }, + { + name: 'params', + algebraicType: ProductType.getTypeScriptAlgebraicType(), + }, + { + name: 'returnType', + algebraicType: AlgebraicType.getTypeScriptAlgebraicType(), + } + ); + return _cached_RawProcedureDefV9_type_value; + }, + + serialize(writer: __BinaryWriter, value: RawProcedureDefV9): void { + __AlgebraicTypeValue.serializeValue( + writer, + RawProcedureDefV9.getTypeScriptAlgebraicType(), + value + ); + }, + + deserialize(reader: __BinaryReader): RawProcedureDefV9 { + return __AlgebraicTypeValue.deserializeValue( + reader, + RawProcedureDefV9.getTypeScriptAlgebraicType() + ); + }, +}; + +export default RawProcedureDefV9; diff --git a/crates/bindings-typescript/src/lib/autogen/raw_view_def_v_9_type.ts b/crates/bindings-typescript/src/lib/autogen/raw_view_def_v_9_type.ts new file mode 100644 index 00000000000..48818599501 --- /dev/null +++ b/crates/bindings-typescript/src/lib/autogen/raw_view_def_v_9_type.ts @@ -0,0 +1,82 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + AlgebraicType as __AlgebraicTypeValue, + BinaryReader as __BinaryReader, + BinaryWriter as __BinaryWriter, + ConnectionId as __ConnectionId, + Identity as __Identity, + TimeDuration as __TimeDuration, + Timestamp as __Timestamp, + deepEqual as __deepEqual, + type AlgebraicType as __AlgebraicTypeType, + type AlgebraicTypeVariants as __AlgebraicTypeVariants, + type TableHandle as __TableHandle, +} from '../../index'; +import { AlgebraicType } from './algebraic_type_type'; +// Mark import as potentially unused +declare type __keep_AlgebraicType = AlgebraicType; +import { ProductType } from './product_type_type'; +// Mark import as potentially unused +declare type __keep_ProductType = ProductType; + +export type RawViewDefV9 = { + name: string; + index: number; + isPublic: boolean; + isAnonymous: boolean; + params: ProductType; + returnType: AlgebraicType; +}; +let _cached_RawViewDefV9_type_value: __AlgebraicTypeType | null = null; + +/** + * An object for generated helper functions. + */ +export const RawViewDefV9 = { + /** + * A function which returns this type represented as an AlgebraicType. + * This function is derived from the AlgebraicType used to generate this type. + */ + getTypeScriptAlgebraicType(): __AlgebraicTypeType { + if (_cached_RawViewDefV9_type_value) return _cached_RawViewDefV9_type_value; + _cached_RawViewDefV9_type_value = __AlgebraicTypeValue.Product({ + elements: [], + }); + _cached_RawViewDefV9_type_value.value.elements.push( + { name: 'name', algebraicType: __AlgebraicTypeValue.String }, + { name: 'index', algebraicType: __AlgebraicTypeValue.U32 }, + { name: 'isPublic', algebraicType: __AlgebraicTypeValue.Bool }, + { name: 'isAnonymous', algebraicType: __AlgebraicTypeValue.Bool }, + { + name: 'params', + algebraicType: ProductType.getTypeScriptAlgebraicType(), + }, + { + name: 'returnType', + algebraicType: AlgebraicType.getTypeScriptAlgebraicType(), + } + ); + return _cached_RawViewDefV9_type_value; + }, + + serialize(writer: __BinaryWriter, value: RawViewDefV9): void { + __AlgebraicTypeValue.serializeValue( + writer, + RawViewDefV9.getTypeScriptAlgebraicType(), + value + ); + }, + + deserialize(reader: __BinaryReader): RawViewDefV9 { + return __AlgebraicTypeValue.deserializeValue( + reader, + RawViewDefV9.getTypeScriptAlgebraicType() + ); + }, +}; + +export default RawViewDefV9; diff --git a/crates/bindings-typescript/src/lib/option.ts b/crates/bindings-typescript/src/lib/option.ts index c6f719a89fe..40e11d2a9b2 100644 --- a/crates/bindings-typescript/src/lib/option.ts +++ b/crates/bindings-typescript/src/lib/option.ts @@ -1,10 +1,10 @@ import { AlgebraicType } from './algebraic_type'; -export type OptionAlgebraicType = { +export type OptionAlgebraicType = { tag: 'Sum'; value: { variants: [ - { name: 'some'; algebraicType: AlgebraicType }, + { name: 'some'; algebraicType: T }, { name: 'none'; algebraicType: { tag: 'Product'; value: { elements: [] } }; @@ -14,9 +14,13 @@ export type OptionAlgebraicType = { }; export const Option: { - getAlgebraicType(innerType: AlgebraicType): OptionAlgebraicType; + getAlgebraicType( + innerType: T + ): OptionAlgebraicType; } = { - getAlgebraicType(innerType: AlgebraicType): OptionAlgebraicType { + getAlgebraicType( + innerType: T + ): OptionAlgebraicType { return AlgebraicType.Sum({ variants: [ { name: 'some', algebraicType: innerType }, diff --git a/crates/bindings-typescript/src/server/indexes.ts b/crates/bindings-typescript/src/server/indexes.ts index e126d48e136..be3185d207f 100644 --- a/crates/bindings-typescript/src/server/indexes.ts +++ b/crates/bindings-typescript/src/server/indexes.ts @@ -46,6 +46,13 @@ export type Indexes< [k in keyof I]: Index; }; +export type ReadonlyIndexes< + TableDef extends UntypedTableDef, + I extends Record>, +> = { + [k in keyof I]: ReadonlyIndex; +}; + /** * A type representing a database index, which can be either unique or ranged. */ @@ -56,32 +63,51 @@ export type Index< ? UniqueIndex : RangedIndex; +export type ReadonlyIndex< + TableDef extends UntypedTableDef, + I extends UntypedIndex, +> = I['unique'] extends true + ? ReadonlyUniqueIndex + : ReadonlyRangedIndex; + +export interface ReadonlyUniqueIndex< + TableDef extends UntypedTableDef, + I extends UntypedIndex, +> { + find(col_val: IndexVal): RowType | null; +} + /** * A type representing a unique index on a database table. * Unique indexes enforce that the indexed columns contain unique values. */ -export type UniqueIndex< +export interface UniqueIndex< TableDef extends UntypedTableDef, I extends UntypedIndex, -> = { - find(col_val: IndexVal): RowType | null; +> extends ReadonlyUniqueIndex { delete(col_val: IndexVal): boolean; update(col_val: RowType): RowType; -}; +} + +export interface ReadonlyRangedIndex< + TableDef extends UntypedTableDef, + I extends UntypedIndex, +> { + filter( + range: IndexScanRangeBounds + ): IterableIterator>; +} /** * A type representing a ranged index on a database table. * Ranged indexes allow for range queries on the indexed columns. */ -export type RangedIndex< +export interface RangedIndex< TableDef extends UntypedTableDef, I extends UntypedIndex, -> = { - filter( - range: IndexScanRangeBounds - ): IterableIterator>; +> extends ReadonlyRangedIndex { delete(range: IndexScanRangeBounds): number; -}; +} /** * A helper type to extract the value type of an index based on the table definition and index definition. diff --git a/crates/bindings-typescript/src/server/register_hooks.ts b/crates/bindings-typescript/src/server/register_hooks.ts index 4ebee6df628..c6d31521c4e 100644 --- a/crates/bindings-typescript/src/server/register_hooks.ts +++ b/crates/bindings-typescript/src/server/register_hooks.ts @@ -1,4 +1,6 @@ import { register_hooks } from 'spacetime:sys@1.0'; -import { hooks } from './runtime'; +import { register_hooks as register_hooks_v1_1 } from 'spacetime:sys@1.1'; +import { hooks, hooks_v1_1 } from './runtime'; register_hooks(hooks); +register_hooks_v1_1(hooks_v1_1); diff --git a/crates/bindings-typescript/src/server/runtime.ts b/crates/bindings-typescript/src/server/runtime.ts index e9d1ecff775..f39e6bdb034 100644 --- a/crates/bindings-typescript/src/server/runtime.ts +++ b/crates/bindings-typescript/src/server/runtime.ts @@ -29,6 +29,13 @@ import { MODULE_DEF } from './schema'; import * as _syscalls from 'spacetime:sys@1.0'; import type { u16, u32, ModuleHooks } from 'spacetime:sys@1.0'; +import { + ANON_VIEWS, + VIEWS, + type AnonymousViewCtx, + type ViewCtx, +} from './views'; +import { bsatnBaseSize } from './util'; const { freeze } = Object; @@ -212,6 +219,46 @@ export const hooks: ModuleHooks = { }, }; +export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = { + __call_view__(id, sender, argsBuf) { + const { fn, params, returnType, returnTypeBaseSize } = VIEWS[id]; + const ctx: ViewCtx = freeze({ + sender: new Identity(sender), + // this is the non-readonly DbView, but the typing for the user will be + // the readonly one, and if they do call mutating functions it will fail + // at runtime + db: getDbView(), + }); + const args = AlgebraicType.deserializeValue( + new BinaryReader(argsBuf), + AlgebraicType.Product(params), + MODULE_DEF.typespace + ); + const ret = fn(ctx, args); + const retBuf = new BinaryWriter(returnTypeBaseSize); + AlgebraicType.serializeValue(retBuf, returnType, ret, MODULE_DEF.typespace); + return retBuf.getBuffer(); + }, + __call_view_anon__(id, argsBuf) { + const { fn, params, returnType, returnTypeBaseSize } = ANON_VIEWS[id]; + const ctx: AnonymousViewCtx = freeze({ + // this is the non-readonly DbView, but the typing for the user will be + // the readonly one, and if they do call mutating functions it will fail + // at runtime + db: getDbView(), + }); + const args = AlgebraicType.deserializeValue( + new BinaryReader(argsBuf), + AlgebraicType.Product(params), + MODULE_DEF.typespace + ); + const ret = fn(ctx, args); + const retBuf = new BinaryWriter(returnTypeBaseSize); + AlgebraicType.serializeValue(retBuf, returnType, ret, MODULE_DEF.typespace); + return retBuf.getBuffer(); + }, +}; + let DB_VIEW: DbView | null = null; function getDbView() { DB_VIEW ??= makeDbView(MODULE_DEF); @@ -464,47 +511,6 @@ function makeTableView(typespace: Typespace, table: RawTableDefV9): Table { return freeze(tableView); } -function bsatnBaseSize(typespace: Typespace, ty: AlgebraicType): number { - const assumedArrayLength = 4; - while (ty.tag === 'Ref') ty = typespace.types[ty.value]; - if (ty.tag === 'Product') { - let sum = 0; - for (const { algebraicType: elem } of ty.value.elements) { - sum += bsatnBaseSize(typespace, elem); - } - return sum; - } else if (ty.tag === 'Sum') { - let min = Infinity; - for (const { algebraicType: vari } of ty.value.variants) { - const vSize = bsatnBaseSize(typespace, vari); - if (vSize < min) min = vSize; - } - if (min === Infinity) min = 0; - return 4 + min; - } else if (ty.tag == 'Array') { - return 4 + assumedArrayLength * bsatnBaseSize(typespace, ty.value); - } - return { - String: 4 + assumedArrayLength, - Sum: 1, - Bool: 1, - I8: 1, - U8: 1, - I16: 2, - U16: 2, - I32: 4, - U32: 4, - F32: 4, - I64: 8, - U64: 8, - F64: 8, - I128: 16, - U128: 16, - I256: 32, - U256: 32, - }[ty.tag]; -} - function hasOwn( o: object, k: K diff --git a/crates/bindings-typescript/src/server/schema.ts b/crates/bindings-typescript/src/server/schema.ts index 6684091b788..d9cdede9a3f 100644 --- a/crates/bindings-typescript/src/server/schema.ts +++ b/crates/bindings-typescript/src/server/schema.ts @@ -22,6 +22,12 @@ import { type AlgebraicTypeVariants, } from '../lib/algebraic_type'; import type RawScopedTypeNameV9 from '../lib/autogen/raw_scoped_type_name_v_9_type'; +import { + defineView, + type AnonymousViewFn, + type ViewFn, + type ViewReturnTypeBuilder, +} from './views'; /** * The global module definition that gets populated by calls to `reducer()` and lifecycle hooks. @@ -176,7 +182,6 @@ class Schema { params: Params, fn: Reducer ): Reducer; - // eslint-disable-next-line @typescript-eslint/no-empty-object-type reducer(name: string, fn: Reducer): Reducer; reducer( name: string, @@ -215,11 +220,8 @@ class Schema { * }); * ``` */ - // eslint-disable-next-line @typescript-eslint/no-empty-object-type init(fn: Reducer): void; - // eslint-disable-next-line @typescript-eslint/no-empty-object-type init(name: string, fn: Reducer): void; - // eslint-disable-next-line @typescript-eslint/no-empty-object-type init(nameOrFn: any, maybeFn?: Reducer): void { const [name, fn] = typeof nameOrFn === 'string' ? [nameOrFn, maybeFn] : ['init', nameOrFn]; @@ -242,11 +244,8 @@ class Schema { * } * ); */ - // eslint-disable-next-line @typescript-eslint/no-empty-object-type clientConnected(fn: Reducer): void; - // eslint-disable-next-line @typescript-eslint/no-empty-object-type clientConnected(name: string, fn: Reducer): void; - // eslint-disable-next-line @typescript-eslint/no-empty-object-type clientConnected(nameOrFn: any, maybeFn?: Reducer): void { const [name, fn] = typeof nameOrFn === 'string' @@ -272,11 +271,8 @@ class Schema { * ); * ``` */ - // eslint-disable-next-line @typescript-eslint/no-empty-object-type clientDisconnected(fn: Reducer): void; - // eslint-disable-next-line @typescript-eslint/no-empty-object-type clientDisconnected(name: string, fn: Reducer): void; - // eslint-disable-next-line @typescript-eslint/no-empty-object-type clientDisconnected(nameOrFn: any, maybeFn?: Reducer): void { const [name, fn] = typeof nameOrFn === 'string' @@ -285,6 +281,72 @@ class Schema { clientDisconnected(name, {}, fn); } + view( + name: string, + ret: Ret, + fn: ViewFn + ): void { + defineView(name, false, {}, ret, fn); + } + + // TODO: re-enable once parameterized views are supported in SQL + // view( + // name: string, + // ret: Ret, + // fn: ViewFn + // ): void; + // view( + // name: string, + // params: Params, + // ret: Ret, + // fn: ViewFn + // ): void; + // view( + // name: string, + // paramsOrRet: Ret | Params, + // retOrFn: ViewFn | Ret, + // maybeFn?: ViewFn + // ): void { + // if (typeof retOrFn === 'function') { + // defineView(name, false, {}, paramsOrRet as Ret, retOrFn); + // } else { + // defineView(name, false, paramsOrRet as Params, retOrFn, maybeFn!); + // } + // } + + anyonymousView( + name: string, + ret: Ret, + fn: AnonymousViewFn + ): void { + defineView(name, true, {}, ret, fn); + } + + // TODO: re-enable once parameterized views are supported in SQL + // anyonymousView( + // name: string, + // ret: Ret, + // fn: AnonymousViewFn + // ): void; + // anyonymousView( + // name: string, + // params: Params, + // ret: Ret, + // fn: AnonymousViewFn + // ): void; + // anyonymousView( + // name: string, + // paramsOrRet: Ret | Params, + // retOrFn: AnonymousViewFn | Ret, + // maybeFn?: AnonymousViewFn + // ): void { + // if (typeof retOrFn === 'function') { + // defineView(name, true, {}, paramsOrRet as Ret, retOrFn); + // } else { + // defineView(name, true, paramsOrRet as Params, retOrFn, maybeFn!); + // } + // } + clientVisibilityFilter = { sql(filter: string): void { MODULE_DEF.rowLevelSecurity.push({ sql: filter }); diff --git a/crates/bindings-typescript/src/server/sys.d.ts b/crates/bindings-typescript/src/server/sys.d.ts index 93c7a19bc02..8a102453d03 100644 --- a/crates/bindings-typescript/src/server/sys.d.ts +++ b/crates/bindings-typescript/src/server/sys.d.ts @@ -66,3 +66,12 @@ declare module 'spacetime:sys@1.0' { 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; + __call_view_anon__(id: u32, args: Uint8Array): Uint8Array; + }; + + export function register_hooks(hooks: ModuleHooks); +} diff --git a/crates/bindings-typescript/src/server/table.ts b/crates/bindings-typescript/src/server/table.ts index 36dbb8981e3..6c2d584166a 100644 --- a/crates/bindings-typescript/src/server/table.ts +++ b/crates/bindings-typescript/src/server/table.ts @@ -5,7 +5,13 @@ import type RawIndexDefV9 from '../lib/autogen/raw_index_def_v_9_type'; import type RawSequenceDefV9 from '../lib/autogen/raw_sequence_def_v_9_type'; import type RawTableDefV9 from '../lib/autogen/raw_table_def_v_9_type'; import type { AllUnique } from './constraints'; -import type { ColumnIndex, IndexColumns, Indexes, IndexOpts } from './indexes'; +import type { + ColumnIndex, + IndexColumns, + Indexes, + IndexOpts, + ReadonlyIndexes, +} from './indexes'; import { MODULE_DEF, splitName } from './schema'; import { RowBuilder, @@ -131,17 +137,25 @@ export type Table = Prettify< TableMethods & Indexes> >; -/** - * A type representing the methods available on a table. - */ -export type TableMethods = { +export type ReadonlyTable = Prettify< + ReadonlyTableMethods & + ReadonlyIndexes> +>; + +export interface ReadonlyTableMethods { /** Returns the number of rows in the TX state. */ count(): bigint; /** Iterate over all rows in the TX state. Rust Iterator → TS IterableIterator. */ iter(): IterableIterator>; [Symbol.iterator](): IterableIterator>; +} +/** + * A type representing the methods available on a table. + */ +export interface TableMethods + extends ReadonlyTableMethods { /** * Insert and return the inserted row (auto-increment fields filled). * @@ -153,7 +167,7 @@ export type TableMethods = { /** Delete a row equal to `row`. Returns true if something was deleted. */ delete(row: RowType): boolean; -}; +} /** * Represents a handle to a database table, including its name, row type, and row spacetime type. diff --git a/crates/bindings-typescript/src/server/type_builders.ts b/crates/bindings-typescript/src/server/type_builders.ts index ab440838003..4fa2b5484db 100644 --- a/crates/bindings-typescript/src/server/type_builders.ts +++ b/crates/bindings-typescript/src/server/type_builders.ts @@ -109,7 +109,6 @@ type ObjectType = { }; type VariantsObj = Record>; -// eslint-disable-next-line @typescript-eslint/no-empty-object-type type UnitBuilder = ProductBuilder<{}>; type SimpleVariantsObj = Record; @@ -1102,10 +1101,13 @@ export class ArrayBuilder> export class OptionBuilder> extends TypeBuilder< InferTypeOfTypeBuilder | undefined, - OptionAlgebraicType + OptionAlgebraicType> > implements - Defaultable | undefined, OptionAlgebraicType> + Defaultable< + InferTypeOfTypeBuilder | undefined, + OptionAlgebraicType> + > { /** * The phantom value type of the option for TypeScript @@ -1113,7 +1115,7 @@ export class OptionBuilder> readonly value!: Value; constructor(value: Value) { - let innerType: AlgebraicType; + let innerType: InferSpacetimeTypeOfTypeBuilder; if (value instanceof ColumnBuilder) { innerType = value.typeBuilder.algebraicType; } else { @@ -2295,11 +2297,14 @@ export class OptionColumnBuilder< > extends ColumnBuilder< InferTypeOfTypeBuilder | undefined, - OptionAlgebraicType, + OptionAlgebraicType>, M > implements - Defaultable | undefined, OptionAlgebraicType> + Defaultable< + InferTypeOfTypeBuilder | undefined, + OptionAlgebraicType> + > { default( value: InferTypeOfTypeBuilder | undefined diff --git a/crates/bindings-typescript/src/server/util.ts b/crates/bindings-typescript/src/server/util.ts new file mode 100644 index 00000000000..78e72ec405e --- /dev/null +++ b/crates/bindings-typescript/src/server/util.ts @@ -0,0 +1,43 @@ +import type { AlgebraicType } from '../lib/algebraic_type'; +import type Typespace from '../lib/autogen/typespace_type'; + +export function bsatnBaseSize(typespace: Typespace, ty: AlgebraicType): number { + const assumedArrayLength = 4; + while (ty.tag === 'Ref') ty = typespace.types[ty.value]; + if (ty.tag === 'Product') { + let sum = 0; + for (const { algebraicType: elem } of ty.value.elements) { + sum += bsatnBaseSize(typespace, elem); + } + return sum; + } else if (ty.tag === 'Sum') { + let min = Infinity; + for (const { algebraicType: vari } of ty.value.variants) { + const vSize = bsatnBaseSize(typespace, vari); + if (vSize < min) min = vSize; + } + if (min === Infinity) min = 0; + return 4 + min; + } else if (ty.tag == 'Array') { + return 4 + assumedArrayLength * bsatnBaseSize(typespace, ty.value); + } + return { + String: 4 + assumedArrayLength, + Sum: 1, + Bool: 1, + I8: 1, + U8: 1, + I16: 2, + U16: 2, + I32: 4, + U32: 4, + F32: 4, + I64: 8, + U64: 8, + F64: 8, + I128: 16, + U128: 16, + I256: 32, + U256: 32, + }[ty.tag]; +} diff --git a/crates/bindings-typescript/src/server/views.ts b/crates/bindings-typescript/src/server/views.ts new file mode 100644 index 00000000000..b4a5fbd0b23 --- /dev/null +++ b/crates/bindings-typescript/src/server/views.ts @@ -0,0 +1,110 @@ +import { + AlgebraicType, + type AlgebraicTypeVariants, + type ProductType, +} from '../lib/algebraic_type'; +import type { Identity } from '../lib/identity'; +import type { OptionAlgebraicType } from '../lib/option'; +import type { ParamsObj } from './reducers'; +import { MODULE_DEF, type UntypedSchemaDef } from './schema'; +import type { ReadonlyTable } from './table'; +import type { Infer, InferTypeOfRow, TypeBuilder } from './type_builders'; +import { bsatnBaseSize } from './util'; + +export type ViewCtx = Readonly<{ + sender: Identity; + db: ReadonlyDbView; +}>; + +export type AnonymousViewCtx = Readonly<{ + db: ReadonlyDbView; +}>; + +export type ReadonlyDbView = { + readonly [Tbl in SchemaDef['tables'][number] as Tbl['name']]: ReadonlyTable; +}; + +export type ViewFn< + S extends UntypedSchemaDef, + Params extends ParamsObj, + Ret extends ViewReturnTypeBuilder, +> = (ctx: ViewCtx, params: InferTypeOfRow) => Infer; + +export type AnonymousViewFn< + S extends UntypedSchemaDef, + Params extends ParamsObj, + Ret extends ViewReturnTypeBuilder, +> = (ctx: AnonymousViewCtx, params: InferTypeOfRow) => Infer; + +export type ViewReturnTypeBuilder = + | TypeBuilder< + readonly object[], + { tag: 'Array'; value: AlgebraicTypeVariants.Product } + > + | TypeBuilder< + object | undefined, + OptionAlgebraicType + >; + +export function defineView< + S extends UntypedSchemaDef, + const Anonymous extends boolean, + Params extends ParamsObj, + Ret extends ViewReturnTypeBuilder, +>( + name: string, + anon: Anonymous, + params: Params, + ret: Ret, + fn: Anonymous extends true + ? AnonymousViewFn + : ViewFn +) { + const paramType = { + elements: Object.entries(params).map(([n, c]) => ({ + name: n, + algebraicType: c.algebraicType, + })), + }; + + MODULE_DEF.miscExports.push({ + tag: 'View', + value: { + name, + index: (anon ? ANON_VIEWS : VIEWS).length, + isPublic: true, + isAnonymous: anon, + params: paramType, + returnType: ret.algebraicType, + }, + }); + + let returnType = ret.algebraicType; + if (returnType.tag == 'Sum') { + const originalFn = fn; + fn = ((ctx: ViewCtx, args: InferTypeOfRow) => { + const ret = originalFn(ctx, args); + return ret == null ? [] : [ret]; + }) as any; + returnType = AlgebraicType.Array( + returnType.value.variants[0].algebraicType + ); + } + + (anon ? ANON_VIEWS : VIEWS).push({ + fn, + params: paramType, + returnType, + returnTypeBaseSize: bsatnBaseSize(MODULE_DEF.typespace, returnType), + }); +} + +type ViewInfo = { + fn: F; + params: ProductType; + returnType: AlgebraicType; + returnTypeBaseSize: number; +}; + +export const VIEWS: ViewInfo>[] = []; +export const ANON_VIEWS: ViewInfo>[] = []; diff --git a/crates/core/src/db/relational_db.rs b/crates/core/src/db/relational_db.rs index bd43f409990..a1100a858ce 100644 --- a/crates/core/src/db/relational_db.rs +++ b/crates/core/src/db/relational_db.rs @@ -2325,7 +2325,7 @@ mod tests { use spacetimedb_data_structures::map::IntMap; use spacetimedb_datastore::error::{DatastoreError, IndexError}; use spacetimedb_datastore::execution_context::ReducerContext; - use spacetimedb_datastore::locking_tx_datastore::ViewCall; + use spacetimedb_datastore::locking_tx_datastore::{FuncCallType, ViewCall}; use spacetimedb_datastore::system_tables::{ system_tables, StConstraintRow, StIndexRow, StSequenceRow, StTableRow, ST_CONSTRAINT_ID, ST_INDEX_ID, ST_SEQUENCE_ID, ST_TABLE_ID, @@ -2978,8 +2978,8 @@ mod tests { "view should not be materialized as read set is not recorded yet" ); - let view_call = Some(ViewCall::anonymous(view_id, args)); - tx.record_table_scan(view_call, table_id); + let view_call = FuncCallType::View(ViewCall::anonymous(view_id, args)); + tx.record_table_scan(&view_call, table_id); assert!( tx.is_materialized(view_name, vec![].into(), sender)?.0, "view should be materialized as read set is recorded" diff --git a/crates/core/src/host/instance_env.rs b/crates/core/src/host/instance_env.rs index d43b4b8e739..94a725b57b4 100644 --- a/crates/core/src/host/instance_env.rs +++ b/crates/core/src/host/instance_env.rs @@ -9,7 +9,7 @@ use core::mem; use parking_lot::{Mutex, MutexGuard}; use smallvec::SmallVec; use spacetimedb_datastore::locking_tx_datastore::state_view::StateView; -use spacetimedb_datastore::locking_tx_datastore::{MutTxId, ViewCall}; +use spacetimedb_datastore::locking_tx_datastore::{FuncCallType, MutTxId}; use spacetimedb_lib::{ConnectionId, Identity, Timestamp}; use spacetimedb_primitives::{ColId, ColList, IndexId, TableId}; use spacetimedb_sats::{ @@ -22,6 +22,7 @@ use spacetimedb_table::table::RowRef; use std::fmt::Display; use std::ops::DerefMut; use std::sync::Arc; +use std::time::Instant; use std::vec::IntoIter; #[derive(Clone)] @@ -29,9 +30,14 @@ pub struct InstanceEnv { pub replica_ctx: Arc, pub scheduler: Scheduler, pub tx: TxSlot, - /// The timestamp the current reducer began running. + /// The timestamp the current function began running. pub start_time: Timestamp, - pub view: Option, + /// The instant the current function began running. + pub start_instant: Instant, + /// The type of the last, including current, function to be executed by this environment. + pub func_type: FuncCallType, + /// The name of the last, including current, function to be executed by this environment. + pub func_name: String, } #[derive(Clone, Default)] @@ -173,7 +179,11 @@ impl InstanceEnv { scheduler, tx: TxSlot::default(), start_time: Timestamp::now(), - view: None, + start_instant: Instant::now(), + // arbitrary - change if we need to recognize that an `InstanceEnv` has never + // run a function + func_type: FuncCallType::Reducer, + func_name: String::from(""), } } @@ -182,16 +192,12 @@ impl InstanceEnv { &self.replica_ctx.database.database_identity } - /// Signal to this `InstanceEnv` that a reducer, procedure call is beginning. - pub fn start_funcall(&mut self, ts: Timestamp) { + /// Signal to this `InstanceEnv` that a function call is beginning. + pub fn start_funcall(&mut self, name: &str, ts: Timestamp, func_type: FuncCallType) { self.start_time = ts; - self.view = None; - } - - /// Signal to this `InstanceEnv` that a view is starting. - pub fn start_view(&mut self, ts: Timestamp, view: ViewCall) { - self.start_time = ts; - self.view = Some(view); + self.start_instant = Instant::now(); + self.func_type = func_type; + name.clone_into(&mut self.func_name); } fn get_tx(&self) -> Result + '_, GetTxError> { @@ -472,7 +478,7 @@ impl InstanceEnv { stdb.table_row_count_mut(tx, table_id) .ok_or(NodesError::TableNotFound) .inspect(|_| { - tx.record_table_scan(self.view.clone(), table_id); + tx.record_table_scan(&self.func_type, table_id); }) } @@ -497,7 +503,7 @@ impl InstanceEnv { &mut bytes_scanned, ); - tx.record_table_scan(self.view.clone(), table_id); + tx.record_table_scan(&self.func_type, table_id); tx.metrics.rows_scanned += rows_scanned; tx.metrics.bytes_scanned += bytes_scanned; @@ -528,7 +534,7 @@ impl InstanceEnv { // Scan the index and serialize rows to bsatn let chunks = ChunkedWriter::collect_iter(pool, iter, &mut rows_scanned, &mut bytes_scanned); - tx.record_index_scan(self.view.clone(), table_id, index_id, lower, upper); + tx.record_index_scan(&self.func_type, table_id, index_id, lower, upper); tx.metrics.index_seeks += 1; tx.metrics.rows_scanned += rows_scanned; @@ -659,16 +665,7 @@ mod test { fn instance_env(db: Arc) -> Result<(InstanceEnv, tokio::runtime::Runtime)> { let (scheduler, _) = Scheduler::open(db.clone()); let (replica_context, runtime) = replica_ctx(db)?; - Ok(( - InstanceEnv { - replica_ctx: Arc::new(replica_context), - scheduler, - tx: TxSlot::default(), - start_time: Timestamp::now(), - view: None, - }, - runtime, - )) + Ok((InstanceEnv::new(Arc::new(replica_context), scheduler), runtime)) } /// An in-memory `RelationalDB` for testing. diff --git a/crates/core/src/host/module_host.rs b/crates/core/src/host/module_host.rs index fc090ddcfec..000c630999b 100644 --- a/crates/core/src/host/module_host.rs +++ b/crates/core/src/host/module_host.rs @@ -408,7 +408,7 @@ impl Instance { fn call_view(&mut self, tx: MutTxId, params: CallViewParams) -> ViewCallResult { match self { Instance::Wasm(inst) => inst.call_view(tx, params), - Instance::Js(_inst) => unimplemented!("JS views are not implemented yet"), + Instance::Js(inst) => inst.call_view(tx, params), } } diff --git a/crates/core/src/host/v8/budget.rs b/crates/core/src/host/v8/budget.rs index db43b9f8387..c127ce2c55a 100644 --- a/crates/core/src/host/v8/budget.rs +++ b/crates/core/src/host/v8/budget.rs @@ -55,7 +55,7 @@ pub(super) extern "C" fn cb_log_long_running(isolate: &mut Isolate, _: *mut c_vo return; }; let database = env.instance_env.replica_ctx.database_identity; - let reducer = env.reducer_name(); + let reducer = env.funcall_name(); let dur = env.reducer_start().elapsed(); tracing::warn!(reducer, ?database, "JavaScript has been running for {dur:?}"); } diff --git a/crates/core/src/host/v8/mod.rs b/crates/core/src/host/v8/mod.rs index 8a7f6b58d0b..a872a4b6824 100644 --- a/crates/core/src/host/v8/mod.rs +++ b/crates/core/src/host/v8/mod.rs @@ -6,15 +6,19 @@ use self::error::{ use self::ser::serialize_to_js; use self::string::{str_from_ident, IntoJsString}; use self::syscall::{ - call_call_reducer, call_describe_module, get_hook, resolve_sys_module, FnRet, HookFunction, ModuleHookKey, + call_call_reducer, call_call_view, call_call_view_anon, call_describe_module, get_hooks, resolve_sys_module, FnRet, + HookFunctions, }; use super::module_common::{build_common_module_from_raw, run_describer, ModuleCommon}; use super::module_host::{CallProcedureParams, CallReducerParams, Module, ModuleInfo, ModuleRuntime}; use super::UpdateDatabaseResult; use crate::host::instance_env::{ChunkPool, InstanceEnv}; -use crate::host::module_host::Instance; +use crate::host::module_host::{CallViewParams, Instance, ViewCallResult}; +use crate::host::v8::error::{ErrorOrException, ExceptionThrown}; use crate::host::wasm_common::instrumentation::CallTimes; -use crate::host::wasm_common::module_host_actor::{DescribeError, ExecuteResult, ExecutionTimings, InstanceCommon}; +use crate::host::wasm_common::module_host_actor::{ + DescribeError, ExecutionStats, ExecutionTimings, InstanceCommon, ReducerExecuteResult, ViewExecuteResult, +}; use crate::host::wasm_common::{RowIters, TimingSpanSet}; use crate::host::{ReducerCallResult, Scheduler}; use crate::module_host_context::{ModuleCreationContext, ModuleCreationContextLimited}; @@ -23,7 +27,8 @@ use crate::util::asyncify; use anyhow::Context as _; use core::str; use itertools::Either; -use spacetimedb_datastore::locking_tx_datastore::MutTxId; +use spacetimedb_client_api_messages::energy::FunctionBudget; +use spacetimedb_datastore::locking_tx_datastore::{FuncCallType, MutTxId, ViewCall}; use spacetimedb_datastore::traits::Program; use spacetimedb_lib::{RawModuleDef, Timestamp}; use spacetimedb_schema::auto_migrate::MigrationPolicy; @@ -165,18 +170,12 @@ struct JsInstanceEnv { /// Track time spent in module-defined spans. timing_spans: TimingSpanSet, - /// The point in time the last reducer call started at. - reducer_start: Instant, - /// Track time spent in all wasm instance env calls (aka syscall time). /// /// Each function, like `insert`, will add the `Duration` spent in it /// to this tracker. call_times: CallTimes, - /// The last, including current, reducer to be executed by this environment. - reducer_name: String, - /// A pool of unused allocated chunks that can be reused. // TODO(Centril): consider using this pool for `console_timer_start` and `bytes_sink_write`. chunk_pool: ChunkPool, @@ -187,10 +186,8 @@ impl JsInstanceEnv { fn new(instance_env: InstanceEnv) -> Self { Self { instance_env, - reducer_start: Instant::now(), call_times: CallTimes::new(), iters: <_>::default(), - reducer_name: "".into(), chunk_pool: <_>::default(), timing_spans: <_>::default(), } @@ -200,34 +197,32 @@ impl JsInstanceEnv { /// /// Returns the handle used by reducers to read from `args` /// as well as the handle used to write the error message, if any. - fn start_reducer(&mut self, name: &str, ts: Timestamp) { - self.reducer_start = Instant::now(); - name.clone_into(&mut self.reducer_name); - self.instance_env.start_funcall(ts); + fn start_funcall(&mut self, name: &str, ts: Timestamp, func_type: FuncCallType) { + self.instance_env.start_funcall(name, ts, func_type); } /// Returns the name of the most recent reducer to be run in this environment. - fn reducer_name(&self) -> &str { - &self.reducer_name + fn funcall_name(&self) -> &str { + &self.instance_env.func_name } /// Returns the name of the most recent reducer to be run in this environment, /// or `None` if no reducer is actively being invoked. fn log_record_function(&self) -> Option<&str> { - let function = self.reducer_name(); + let function = self.funcall_name(); (!function.is_empty()).then_some(function) } /// Returns the name of the most recent reducer to be run in this environment. fn reducer_start(&self) -> Instant { - self.reducer_start + self.instance_env.start_instant } /// Signal to this `WasmInstanceEnv` that a reducer call is over. /// This resets all of the state associated to a single reducer call, /// and returns instrumentation records. fn finish_reducer(&mut self) -> ExecutionTimings { - let total_duration = self.reducer_start.elapsed(); + let total_duration = self.reducer_start().elapsed(); // Taking the call times record also resets timings to 0s for the next call. let wasm_instance_env_call_times = self.call_times.take(); @@ -251,6 +246,7 @@ pub struct JsInstance { request_tx: SyncSender, update_response_rx: Receiver>, call_reducer_response_rx: Receiver<(ReducerCallResult, bool)>, + call_view_response_rx: Receiver<(ViewCallResult, bool)>, trapped: bool, } @@ -305,6 +301,24 @@ impl JsInstance { ) -> Result { todo!("JS/TS module procedure support") } + + pub fn call_view(&mut self, tx: MutTxId, params: CallViewParams) -> ViewCallResult { + // Send the request. + let request = JsWorkerRequest::CallView { tx, params }; + self.request_tx + .send(request) + .expect("worker's `request_rx` should be live as `JsInstance::drop` hasn't happened"); + + // Wait for the response. + let (response, trapped) = self + .call_view_response_rx + .recv() + .expect("worker's `call_view_response_tx` should be live as `JsInstance::drop` hasn't happened"); + + self.trapped = trapped; + + response + } } /// A request for the worker in [`spawn_instance_worker`]. @@ -323,6 +337,8 @@ enum JsWorkerRequest { tx: Option, params: CallReducerParams, }, + /// See [`JsInstance::call_view`]. + CallView { tx: MutTxId, params: CallViewParams }, } /// Performs some of the startup work of [`spawn_instance_worker`]. @@ -332,26 +348,25 @@ fn startup_instance_worker<'scope>( scope: &mut PinScope<'scope, '_>, program: Arc, module_or_mcc: Either, -) -> anyhow::Result<(HookFunction<'scope>, Either)> { +) -> anyhow::Result<(HookFunctions<'scope>, Either)> { // Start-up the user's module. eval_user_module_catch(scope, &program).map_err(DescribeError::Setup)?; // Find the `__call_reducer__` function. - let call_reducer_fun = - get_hook(scope, ModuleHookKey::CallReducer).context("The `spacetimedb/server` module was never imported")?; + let hook_functions = get_hooks(scope).context("The `spacetimedb/server` module was never imported")?; // If we don't have a module, make one. let module_common = match module_or_mcc { Either::Left(module_common) => Either::Left(module_common), Either::Right(mcc) => { - let def = extract_description(scope, &mcc.replica_ctx)?; + let def = extract_description(scope, &hook_functions, &mcc.replica_ctx)?; // Validate and create a common module from the raw definition. Either::Right(build_common_module_from_raw(mcc, def)?) } }; - Ok((call_reducer_fun, module_common)) + Ok((hook_functions, module_common)) } /// Returns a new isolate. @@ -385,6 +400,8 @@ fn spawn_instance_worker( let (update_response_tx, update_response_rx) = mpsc::sync_channel(0); // The Worker --ReducerCallResult-> Instance channel: let (call_reducer_response_tx, call_reducer_response_rx) = mpsc::sync_channel(0); + // The Worker --ViewCallResult-> Instance channel: + let (call_view_response_tx, call_view_response_rx) = mpsc::sync_channel(0); // This one-shot channel is used for initial startup error handling within the thread. let (result_tx, result_rx) = oneshot::channel(); @@ -400,7 +417,7 @@ fn spawn_instance_worker( unreachable!("should have a live receiver"); } }; - let (call_reducer_fun, module_common) = match startup_instance_worker(scope, program, module_or_mcc) { + let (hooks, module_common) = match startup_instance_worker(scope, program, module_or_mcc) { Err(err) => { // There was some error in module setup. // Return the error and terminate the worker. @@ -449,7 +466,7 @@ fn spawn_instance_worker( // but rather let this happen by `return_instance` using `JsInstance::trapped` // which will cause `JsInstance` to be dropped, // which in turn results in the loop being terminated. - let res = call_reducer(&mut instance_common, replica_ctx, scope, call_reducer_fun, tx, params); + let res = call_reducer(&mut instance_common, replica_ctx, scope, &hooks, tx, params); // Reply to `JsInstance::call_reducer`. if let Err(e) = call_reducer_response_tx.send(res) { @@ -458,6 +475,13 @@ fn spawn_instance_worker( unreachable!("should have receiver for `call_reducer` response, {e}"); } } + JsWorkerRequest::CallView { tx, params } => { + let res = call_view(&mut instance_common, replica_ctx, scope, &hooks, tx, params); + + if let Err(e) = call_view_response_tx.send(res) { + unreachable!("should have receiver for `call_view` response, {e}"); + } + } } } }); @@ -469,6 +493,7 @@ fn spawn_instance_worker( request_tx, update_response_rx, call_reducer_response_rx, + call_view_response_rx, trapped: false, }; (opt_mc, inst) @@ -578,11 +603,68 @@ fn call_free_fun<'scope>( fun.call(scope, receiver, args).ok_or_else(exception_already_thrown) } +#[allow(clippy::too_many_arguments)] +fn common_call<'scope, R>( + scope: &mut PinScope<'scope, '_>, + tx: MutTxId, + name: &str, + timestamp: Timestamp, + budget: FunctionBudget, + func_type: FuncCallType, + trapped: &mut bool, + call: impl FnOnce(&mut PinScope<'scope, '_>) -> Result>, +) -> (MutTxId, ExecutionStats, anyhow::Result) { + // TODO(v8): Start the budget timeout and long-running logger. + let env = env_on_isolate_unwrap(scope); + let mut tx_slot = env.instance_env.tx.clone(); + + // Start the timer. + // We'd like this tightly around `call`. + env.start_funcall(name, timestamp, func_type); + + // Call the function with `tx` provided. + // It should not be available before. + let (tx, call_result) = tx_slot.set(tx, || { + catch_exception(scope, call).map_err(|(e, can_continue)| { + // Convert `can_continue` to whether the isolate has "trapped". + // Also cancel execution termination if needed, + // that can occur due to terminating long running reducers. + *trapped = match can_continue { + CanContinue::No => false, + CanContinue::Yes => true, + CanContinue::YesCancelTermination => { + scope.cancel_terminate_execution(); + true + } + }; + + anyhow::Error::from(e) + }) + }); + + // Finish timings. + let timings = env_on_isolate_unwrap(scope).finish_reducer(); + + // Derive energy stats. + let energy = energy_from_elapsed(budget, timings.total_duration); + + // Fetch the currently used heap size in V8. + // The used size is ostensibly fairer than the total size. + let memory_allocation = scope.get_heap_statistics().used_heap_size(); + + let stats = ExecutionStats { + energy, + timings, + memory_allocation, + }; + (tx, stats, call_result) +} + fn call_reducer<'scope>( instance_common: &mut InstanceCommon, replica_ctx: &ReplicaContext, scope: &mut PinScope<'scope, '_>, - fun: HookFunction<'_>, + hooks: &HookFunctions<'_>, tx: Option, params: CallReducerParams, ) -> (super::ReducerCallResult, bool) { @@ -594,56 +676,50 @@ fn call_reducer<'scope>( params, move |a, b, c| log_traceback(replica_ctx, a, b, c), |tx, op, budget| { - // TODO(v8): Start the budget timeout and long-running logger. - let env = env_on_isolate_unwrap(scope); - let mut tx_slot = env.instance_env.tx.clone(); - - // Start the timer. - // We'd like this tightly around `__call_reducer__`. - env.start_reducer(op.name, op.timestamp); - - // Call `__call_reducer__` with `tx` provided. - // It should not be available before. - let (tx, call_result) = tx_slot.set(tx, || { - catch_exception(scope, |scope| { - let res = call_call_reducer(scope, fun, op)?; + let func = FuncCallType::Reducer; + let (tx, stats, call_result) = + common_call(scope, tx, op.name, op.timestamp, budget, func, &mut trapped, |scope| { + let res = call_call_reducer(scope, hooks, op)?; Ok(res) - }) - .map_err(|(e, can_continue)| { - // Convert `can_continue` to whether the isolate has "trapped". - // Also cancel execution termination if needed, - // that can occur due to terminating long running reducers. - trapped = match can_continue { - CanContinue::No => false, - CanContinue::Yes => true, - CanContinue::YesCancelTermination => { - scope.cancel_terminate_execution(); - true - } - }; - - e - }) - .map_err(anyhow::Error::from) - }); - - // Finish timings. - let timings = env_on_isolate_unwrap(scope).finish_reducer(); + }); + (tx, ReducerExecuteResult { stats, call_result }) + }, + ); - // Derive energy stats. - let energy = energy_from_elapsed(budget, timings.total_duration); + (res, trapped) +} - // Fetch the currently used heap size in V8. - // The used size is ostensibly fairer than the total size. - let memory_allocation = scope.get_heap_statistics().used_heap_size(); +fn call_view<'scope>( + instance_common: &mut InstanceCommon, + replica_ctx: &ReplicaContext, + scope: &mut PinScope<'scope, '_>, + hooks: &HookFunctions<'_>, + tx: MutTxId, + params: CallViewParams, +) -> (ViewCallResult, bool) { + let mut trapped = false; - let exec_result = ExecuteResult { - energy, - timings, - memory_allocation, - call_result, - }; - (tx, exec_result) + let is_anonymous = params.is_anonymous; + let (res, _) = instance_common.call_view_with_tx( + replica_ctx, + tx, + params, + move |a, b, c| log_traceback(replica_ctx, a, b, c), + |tx, op, budget| { + let func = FuncCallType::View(if is_anonymous { + ViewCall::anonymous(op.db_id, op.args.get_bsatn().clone()) + } else { + ViewCall::with_identity(*op.caller_identity, op.db_id, op.args.get_bsatn().clone()) + }); + let (tx, stats, call_result) = + common_call(scope, tx, op.name, op.timestamp, budget, func, &mut trapped, |scope| { + Ok(if is_anonymous { + call_call_view_anon(scope, hooks, op.into())? + } else { + call_call_view(scope, hooks, op)? + }) + }); + (tx, ViewExecuteResult { stats, call_result }) }, ); @@ -653,19 +729,17 @@ fn call_reducer<'scope>( /// Extracts the raw module def by running the registered `__describe_module__` hook. fn extract_description<'scope>( scope: &mut PinScope<'scope, '_>, + hooks: &HookFunctions<'_>, replica_ctx: &ReplicaContext, ) -> Result { run_describer( |a, b, c| log_traceback(replica_ctx, a, b, c), || { catch_exception(scope, |scope| { - let describe_module = get_hook(scope, ModuleHookKey::DescribeModule) - .context("The `spacetimedb/server` package was never imported into the module")?; - let def = call_describe_module(scope, describe_module)?; + let def = call_describe_module(scope, hooks)?; Ok(def) }) - .map_err(|(e, _)| e) - .map_err(Into::into) + .map_err(|(e, _)| e.into()) }, ) } @@ -699,7 +773,7 @@ mod test { fn call_call_reducer_works() { let call = |code| { with_module_catch(code, |scope| { - let fun = get_hook(scope, ModuleHookKey::CallReducer).unwrap(); + let hooks = get_hooks(scope).unwrap(); let op = ReducerOp { id: ReducerId(42), name: "foobar", @@ -708,7 +782,7 @@ mod test { timestamp: Timestamp::from_micros_since_unix_epoch(24), args: &ArgsTuple::nullary(), }; - Ok(call_call_reducer(scope, fun, op)?) + Ok(call_call_reducer(scope, &hooks, op)?) }) }; @@ -778,8 +852,8 @@ js error Uncaught Error: foobar }) "#; let raw_mod = with_module_catch(code, |scope| { - let describe_module = get_hook(scope, ModuleHookKey::DescribeModule).unwrap(); - call_describe_module(scope, describe_module) + let hooks = get_hooks(scope).unwrap(); + call_describe_module(scope, &hooks) }) .map_err(|e| e.to_string()); assert_eq!(raw_mod, Ok(RawModuleDef::V9(<_>::default()))); diff --git a/crates/core/src/host/v8/syscall/hooks.rs b/crates/core/src/host/v8/syscall/hooks.rs index 05acaf48e04..e5143a3a651 100644 --- a/crates/core/src/host/v8/syscall/hooks.rs +++ b/crates/core/src/host/v8/syscall/hooks.rs @@ -48,6 +48,8 @@ pub(super) fn set_hook_slots( pub(in super::super) enum ModuleHookKey { DescribeModule, CallReducer, + CallView, + CallAnonymousView, } impl ModuleHookKey { @@ -59,6 +61,8 @@ impl ModuleHookKey { // reverted to just 0, 1... once denoland/rusty_v8#1868 merges ModuleHookKey::DescribeModule => 20, ModuleHookKey::CallReducer => 21, + ModuleHookKey::CallView => 22, + ModuleHookKey::CallAnonymousView => 23, } } } @@ -95,29 +99,37 @@ impl HooksInfo { fn register(&self, hook: ModuleHookKey) -> Result<(), ()> { self.registered[hook].set(()) } - - /// Returns the `AbiVersion` for the given `hook`, if any. - fn get(&self, hook: ModuleHookKey) -> Option { - self.registered[hook].get().map(|_| self.abi) - } } #[derive(Copy, Clone)] -/// The actual callable module hook function and its abi version. -pub(in super::super) struct HookFunction<'scope>(pub AbiVersion, pub Local<'scope, Function>); +/// The actual callable module hook functions and their abi version. +pub(in super::super) struct HookFunctions<'scope> { + pub abi: AbiVersion, + /// describe_module and call_reducer existed in v1.0, but everything else is `Option`al + pub describe_module: Local<'scope, Function>, + pub call_reducer: Local<'scope, Function>, + pub call_view: Option>, + pub call_view_anon: Option>, +} /// Returns the hook function previously registered in [`register_hooks`]. -pub(in super::super) fn get_hook<'scope>( - scope: &mut PinScope<'scope, '_>, - hook: ModuleHookKey, -) -> Option> { +pub(in super::super) fn get_hooks<'scope>(scope: &mut PinScope<'scope, '_>) -> Option> { let ctx = scope.get_current_context(); let hooks = ctx.get_slot::()?; - let abi_version = hooks.get(hook)?; + let get = |hook: ModuleHookKey| { + hooks.registered[hook].get().map(|()| { + ctx.get_embedder_data(scope, hook.to_slot_index()) + .expect("if the hook is registered it must have been set") + .cast() + }) + }; - let hooks = ctx - .get_embedder_data(scope, hook.to_slot_index()) - .expect("if `AbiVersion` is set the hook must be set"); - Some(HookFunction(abi_version, hooks.cast())) + Some(HookFunctions { + abi: hooks.abi, + describe_module: get(ModuleHookKey::DescribeModule)?, + call_reducer: get(ModuleHookKey::CallReducer)?, + call_view: get(ModuleHookKey::CallView), + call_view_anon: get(ModuleHookKey::CallAnonymousView), + }) } diff --git a/crates/core/src/host/v8/syscall/mod.rs b/crates/core/src/host/v8/syscall/mod.rs index caa58ceb33c..25afa0f4eac 100644 --- a/crates/core/src/host/v8/syscall/mod.rs +++ b/crates/core/src/host/v8/syscall/mod.rs @@ -1,15 +1,16 @@ +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::{ReducerOp, ReducerResult}; +use crate::host::wasm_common::module_host_actor::{AnonymousViewOp, ReducerOp, ReducerResult, ViewOp}; mod hooks; mod v1; -pub(super) use self::hooks::{get_hook, HookFunction, ModuleHookKey}; +pub(super) use self::hooks::{get_hooks, HookFunctions, ModuleHookKey}; /// The return type of a module -> host syscall. pub(super) type FnRet<'scope> = ExcResult>; @@ -52,6 +53,7 @@ fn resolve_sys_module_inner<'scope>( match module { "sys" => match (major, minor) { (1, 0) => Ok(v1::sys_v1_0(scope)), + (1, 1) => Ok(v1::sys_v1_1(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." @@ -67,12 +69,37 @@ fn resolve_sys_module_inner<'scope>( /// This handles any (future) ABI version differences. pub(super) fn call_call_reducer( scope: &mut PinScope<'_, '_>, - fun: HookFunction<'_>, + hooks: &HookFunctions<'_>, op: ReducerOp<'_>, ) -> ExcResult { - let HookFunction(ver, fun) = fun; - match ver { - AbiVersion::V1 => v1::call_call_reducer(scope, fun, op), + match hooks.abi { + AbiVersion::V1 => v1::call_call_reducer(scope, hooks, op), + } +} + +/// Calls the registered `__call_view__` function hook. +/// +/// This handles any (future) ABI version differences. +pub(super) fn call_call_view( + scope: &mut PinScope<'_, '_>, + hooks: &HookFunctions<'_>, + op: ViewOp<'_>, +) -> Result> { + match hooks.abi { + AbiVersion::V1 => v1::call_call_view(scope, hooks, op), + } +} + +/// Calls the registered `__call_view_anon__` function hook. +/// +/// This handles any (future) ABI version differences. +pub(super) fn call_call_view_anon( + scope: &mut PinScope<'_, '_>, + hooks: &HookFunctions<'_>, + op: AnonymousViewOp<'_>, +) -> Result> { + match hooks.abi { + AbiVersion::V1 => v1::call_call_view_anon(scope, hooks, op), } } @@ -81,10 +108,9 @@ pub(super) fn call_call_reducer( /// This handles any (future) ABI version differences. pub(super) fn call_describe_module<'scope>( scope: &mut PinScope<'scope, '_>, - fun: HookFunction<'_>, + hooks: &HookFunctions<'_>, ) -> Result> { - let HookFunction(ver, fun) = fun; - match ver { - AbiVersion::V1 => v1::call_describe_module(scope, fun), + match hooks.abi { + AbiVersion::V1 => v1::call_describe_module(scope, hooks), } } diff --git a/crates/core/src/host/v8/syscall/v1.rs b/crates/core/src/host/v8/syscall/v1.rs index c7f666e258f..082257d70b6 100644 --- a/crates/core/src/host/v8/syscall/v1.rs +++ b/crates/core/src/host/v8/syscall/v1.rs @@ -8,17 +8,19 @@ use crate::host::v8::error::{ErrorOrException, ExcResult, ExceptionThrown}; 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::{ call_free_fun, env_on_isolate, exception_already_thrown, BufferTooSmall, CodeError, JsInstanceEnv, JsStackTrace, TerminationError, Throwable, }; use crate::host::wasm_common::instrumentation::span; -use crate::host::wasm_common::module_host_actor::{ReducerOp, ReducerResult}; +use crate::host::wasm_common::module_host_actor::{AnonymousViewOp, ReducerOp, ReducerResult, ViewOp}; use crate::host::wasm_common::{err_to_errno_and_log, RowIterIdx, TimingSpan, TimingSpanIdx}; use crate::host::AbiCall; use anyhow::Context; +use bytes::Bytes; use spacetimedb_lib::{bsatn, ConnectionId, Identity, RawModuleDef}; -use spacetimedb_primitives::{errno, ColId, IndexId, ReducerId, TableId}; +use spacetimedb_primitives::{errno, ColId, IndexId, ReducerId, TableId, ViewId}; use spacetimedb_sats::Serialize; use v8::{ callback_scope, ConstructorBehavior, Function, FunctionCallbackArguments, Isolate, Local, Module, Object, @@ -26,7 +28,7 @@ use v8::{ }; macro_rules! create_synthetic_module { - ($scope:expr, $module_name:expr, $(($wrapper:ident, $abi_call:expr, $fun:ident),)*) => {{ + ($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); @@ -111,6 +113,11 @@ pub(super) fn sys_v1_0<'scope>(scope: &mut PinScope<'scope, '_>) -> Local<'scope ) } +pub(super) fn sys_v1_1<'scope>(scope: &mut PinScope<'scope, '_>) -> Local<'scope, Module> { + use register_hooks_v1_1 as register_hooks; + create_synthetic_module!(scope, "spacetime:sys@1.1", (with_nothing, (), register_hooks)) +} + /// Registers a function in `module` /// where the function has `name` and does `body`. fn register_module_fun( @@ -332,10 +339,55 @@ fn register_hooks_v1_0<'scope>(scope: &mut PinScope<'scope, '_>, args: FunctionC Ok(v8::undefined(scope).into()) } +/// Module ABI that registers the functions called by the host. +/// +/// # Signature +/// +/// ```ignore +/// register_hooks(hooks: { +/// __call_view__(view_id: u32, sender: u256, args: u8[]): u8[]; +/// __call_view_anon__(view_id: u32, args: u8[]): u8[]; +/// }): void +/// ``` +/// +/// # Types +/// +/// - `u8` is `number` in JS restricted to unsigned 8-bit integers. +/// - `u32` is `bigint` in JS restricted to unsigned 32-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 the correct functions. +fn register_hooks_v1_1<'scope>(scope: &mut PinScope<'scope, '_>, args: FunctionCallbackArguments<'_>) -> FnRet<'scope> { + // Convert `hooks` to an object. + let hooks = cast!(scope, args.get(0), Object, "hooks object").map_err(|e| e.throw(scope))?; + + 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__))?; + + // Set the hooks. + set_hook_slots( + scope, + AbiVersion::V1, + &[ + (ModuleHookKey::CallView, call_view), + (ModuleHookKey::CallAnonymousView, call_view_anon), + ], + )?; + + Ok(v8::undefined(scope).into()) +} + /// Calls the `__call_reducer__` function `fun`. pub(super) fn call_call_reducer( scope: &mut PinScope<'_, '_>, - fun: Local<'_, Function>, + hooks: &HookFunctions<'_>, op: ReducerOp<'_>, ) -> ExcResult { let ReducerOp { @@ -355,7 +407,7 @@ pub(super) fn call_call_reducer( let args = &[reducer_id, sender, conn_id, timestamp, reducer_args]; // Call the function. - let ret = call_free_fun(scope, fun, args)?; + let ret = call_free_fun(scope, hooks.call_reducer, args)?; // Deserialize the user result. let user_res = deserialize_js(scope, ret)?; @@ -363,13 +415,76 @@ pub(super) fn call_call_reducer( Ok(user_res) } +/// 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 { + id: ViewId(view_id), + db_id: _, + name: _, + caller_identity: 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)?; + + // Deserialize the user result. + let ret = cast!(scope, ret, v8::Uint8Array, "bytes return from `__call_view__`").map_err(|e| e.throw(scope))?; + let bytes = ret.get_contents(&mut []); + + Ok(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__` was never defined")?; + + let AnonymousViewOp { + id: ViewId(view_id), + db_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)?; + + // Deserialize the user result. + let ret = + cast!(scope, ret, v8::Uint8Array, "bytes return from `__call_view_anon__`").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<'_, '_>, - fun: Local<'_, Function>, + hooks: &HookFunctions<'_>, ) -> Result> { // Call the function. - let raw_mod_js = call_free_fun(scope, fun, &[])?; + let raw_mod_js = call_free_fun(scope, hooks.describe_module, &[])?; // Deserialize the raw module. let raw_mod = cast!( diff --git a/crates/core/src/host/wasm_common/module_host_actor.rs b/crates/core/src/host/wasm_common/module_host_actor.rs index 64731baf0f6..01c20cc59fe 100644 --- a/crates/core/src/host/wasm_common/module_host_actor.rs +++ b/crates/core/src/host/wasm_common/module_host_actor.rs @@ -66,7 +66,7 @@ pub trait WasmInstance: Send + Sync + 'static { fn instance_env(&self) -> &InstanceEnv; - fn call_reducer(&mut self, op: ReducerOp<'_>, budget: FunctionBudget) -> ExecuteResult; + fn call_reducer(&mut self, op: ReducerOp<'_>, budget: FunctionBudget) -> ReducerExecuteResult; fn call_view(&mut self, op: ViewOp<'_>, budget: FunctionBudget) -> ViewExecuteResult; @@ -113,62 +113,32 @@ impl ExecutionTimings { /// The result that `__call_reducer__` produces during normal non-trap execution. pub type ReducerResult = Result<(), Box>; -pub struct ExecuteResult { +pub struct ExecutionStats { pub energy: EnergyStats, pub timings: ExecutionTimings, pub memory_allocation: usize, +} + +#[derive(derive_more::AsRef)] +pub struct ReducerExecuteResult { + #[as_ref] + pub stats: ExecutionStats, pub call_result: anyhow::Result, } +#[derive(derive_more::AsRef)] pub struct ViewExecuteResult { - pub energy: EnergyStats, - pub timings: ExecutionTimings, - pub memory_allocation: usize, + #[as_ref] + pub stats: ExecutionStats, pub call_result: anyhow::Result, } +#[derive(derive_more::AsRef)] pub struct ProcedureExecuteResult { - #[allow(unused)] - pub energy: EnergyStats, - #[allow(unused)] - pub timings: ExecutionTimings, - pub memory_allocation: usize, + #[as_ref] + pub stats: ExecutionStats, pub call_result: anyhow::Result, } -trait FunctionResult { - fn energy(&self) -> &EnergyStats; - fn timings(&self) -> &ExecutionTimings; - fn memory_allocation(&self) -> usize; -} - -impl FunctionResult for ExecuteResult { - fn energy(&self) -> &EnergyStats { - &self.energy - } - - fn timings(&self) -> &ExecutionTimings { - &self.timings - } - - fn memory_allocation(&self) -> usize { - self.memory_allocation - } -} - -impl FunctionResult for ViewExecuteResult { - fn energy(&self) -> &EnergyStats { - &self.energy - } - - fn timings(&self) -> &ExecutionTimings { - &self.timings - } - - fn memory_allocation(&self) -> usize { - self.memory_allocation - } -} - pub struct WasmModuleHostActor { module: T::InstancePre, common: ModuleCommon, @@ -509,10 +479,13 @@ impl InstanceCommon { let result = vm_call_procedure(op, budget).await; let ProcedureExecuteResult { - memory_allocation, + stats: + ExecutionStats { + memory_allocation, + // TODO(procedure-energy): Do something with timing and energy. + .. + }, call_result, - // TODO(procedure-energy): Do something with timing and energy. - .. } = result; // TODO(shub): deduplicate with reducer and view logic. @@ -583,7 +556,7 @@ impl InstanceCommon { tx: Option, params: CallReducerParams, log_traceback: impl FnOnce(&str, &str, &anyhow::Error), - vm_call_reducer: impl FnOnce(MutTxId, ReducerOp<'_>, FunctionBudget) -> (MutTxId, ExecuteResult), + vm_call_reducer: impl FnOnce(MutTxId, ReducerOp<'_>, FunctionBudget) -> (MutTxId, ReducerExecuteResult), ) -> (ReducerCallResult, bool) { let CallReducerParams { timestamp, @@ -628,13 +601,13 @@ impl InstanceCommon { }); let mut tx = tx.expect("transaction should be present here"); - let energy_used = result.energy.used(); + let energy_used = result.stats.energy.used(); let energy_quanta_used = energy_used.into(); - let timings = &result.timings; + let timings = &result.stats.timings; vm_metrics.report( energy_used.get(), - result.timings.total_duration, - &result.timings.wasm_instance_env_call_times, + result.stats.timings.total_duration, + &result.stats.timings.wasm_instance_env_call_times, ); let mut trapped = false; @@ -650,7 +623,7 @@ impl InstanceCommon { trapped = true; self.handle_outer_error( - &result.energy, + &result.stats.energy, &caller_identity, &Some(caller_connection_id), reducer_name, @@ -728,7 +701,7 @@ impl InstanceCommon { } /// Calls a function (reducer, view) and performs energy monitoring. - fn call_function( + fn call_function>( &mut self, caller_identity: Identity, function_name: &str, @@ -749,10 +722,11 @@ impl InstanceCommon { let (tx, result) = vm_call_function(budget); - let energy_used = result.energy().used(); + let stats: &ExecutionStats = result.as_ref(); + let energy_used = stats.energy.used(); let energy_quanta_used = energy_used.into(); - let timings = &result.timings(); - let memory_allocation = result.memory_allocation(); + let timings = &stats.timings; + let memory_allocation = stats.memory_allocation; self.energy_monitor .record_reducer(&energy_fingerprint, energy_quanta_used, timings.total_duration); @@ -775,7 +749,7 @@ impl InstanceCommon { /// Similar to `call_reducer_with_tx`, but for views. /// unlike to `call_reducer_with_tx`, It does not handle `tx`creation or commit, /// It returns the updated `tx` instead. - fn call_view_with_tx( + pub(crate) fn call_view_with_tx( &mut self, replica_ctx: &ReplicaContext, tx: MutTxId, @@ -821,7 +795,7 @@ impl InstanceCommon { log_traceback("view", view_name, &err); trapped = true; - self.handle_outer_error(&result.energy, &caller_identity, &caller_connection_id, view_name) + self.handle_outer_error(&result.stats.energy, &caller_identity, &caller_connection_id, view_name) .into() } Ok(res) => { @@ -847,8 +821,8 @@ impl InstanceCommon { let res = ViewCallResult { outcome, tx, - energy_used: result.energy.used().into(), - execution_duration: result.timings.total_duration, + energy_used: result.stats.energy.used().into(), + execution_duration: result.stats.timings.total_duration, }; (res, trapped) diff --git a/crates/core/src/host/wasmtime/wasm_instance_env.rs b/crates/core/src/host/wasmtime/wasm_instance_env.rs index 9b5f752c7a9..e59b45ca5ce 100644 --- a/crates/core/src/host/wasmtime/wasm_instance_env.rs +++ b/crates/core/src/host/wasmtime/wasm_instance_env.rs @@ -9,7 +9,7 @@ use crate::host::wasm_common::{err_to_errno_and_log, RowIterIdx, RowIters, Timin use crate::host::AbiCall; use anyhow::Context as _; use spacetimedb_data_structures::map::IntMap; -use spacetimedb_datastore::locking_tx_datastore::ViewCall; +use spacetimedb_datastore::locking_tx_datastore::FuncCallType; use spacetimedb_lib::{ConnectionId, Timestamp}; use spacetimedb_primitives::{errno, ColId}; use std::future::Future; @@ -104,18 +104,12 @@ pub(super) struct WasmInstanceEnv { /// Track time spent in module-defined spans. timing_spans: TimingSpanSet, - /// The point in time the last, or current, reducer, procedure or view call started at. - funcall_start: Instant, - /// Track time spent in all wasm instance env calls (aka syscall time). /// /// Each function, like `insert`, will add the `Duration` spent in it /// to this tracker. call_times: CallTimes, - /// The name of the last, including current, reducer, procedure, or view to be executed by this environment. - funcall_name: String, - /// A pool of unused allocated chunks that can be reused. // TODO(Centril): consider using this pool for `console_timer_start` and `bytes_sink_write`. chunk_pool: ChunkPool, @@ -126,19 +120,11 @@ const STANDARD_BYTES_SINK: u32 = 1; type WasmResult = Result; type RtResult = anyhow::Result; -/// The type of function call being performed. -pub enum FuncCallType { - Reducer, - Procedure, - View(ViewCall), -} - /// Wraps an `InstanceEnv` with the magic necessary to push /// and pull bytes from webassembly memory. impl WasmInstanceEnv { /// Create a new `WasmEnstanceEnv` from the given `InstanceEnv`. pub fn new(instance_env: InstanceEnv) -> Self { - let funcall_start = Instant::now(); Self { instance_env, mem: None, @@ -147,9 +133,7 @@ impl WasmInstanceEnv { standard_bytes_sink: None, iters: Default::default(), timing_spans: Default::default(), - funcall_start, call_times: CallTimes::new(), - funcall_name: String::from(""), chunk_pool: <_>::default(), } } @@ -246,24 +230,14 @@ impl WasmInstanceEnv { let args = self.create_bytes_source(args).unwrap(); - self.funcall_start = Instant::now(); - name.clone_into(&mut self.funcall_name); - - match func_type { - FuncCallType::Reducer | FuncCallType::Procedure => { - self.instance_env.start_funcall(ts); - } - FuncCallType::View(view) => { - self.instance_env.start_view(ts, view); - } - } + self.instance_env.start_funcall(name, ts, func_type); (args, errors) } /// Returns the name of the most recent reducer or procedure to be run in this environment. pub fn funcall_name(&self) -> &str { - &self.funcall_name + &self.instance_env.func_name } /// Returns the name of the most recent reducer or procedure to be run in this environment, @@ -275,7 +249,7 @@ impl WasmInstanceEnv { /// Returns the start time of the most recent reducer or procedure to be run in this environment. pub fn funcall_start(&self) -> Instant { - self.funcall_start + self.instance_env.start_instant } /// Signal to this `WasmInstanceEnv` that a reducer or procedure call is over. @@ -289,7 +263,7 @@ impl WasmInstanceEnv { // we only explicitly clear the source/sink buffers and the "syscall" times. // TODO: should we be clearing `iters` and/or `timing_spans`? - let total_duration = self.funcall_start.elapsed(); + let total_duration = self.instance_env.start_instant.elapsed(); // Taking the call times record also resets timings to 0s for the next call. let wasm_instance_env_call_times = self.call_times.take(); diff --git a/crates/core/src/host/wasmtime/wasmtime_module.rs b/crates/core/src/host/wasmtime/wasmtime_module.rs index b6f13bf90ef..a4e631040ac 100644 --- a/crates/core/src/host/wasmtime/wasmtime_module.rs +++ b/crates/core/src/host/wasmtime/wasmtime_module.rs @@ -5,12 +5,13 @@ use super::{Mem, WasmtimeFuel, EPOCH_TICKS_PER_SECOND}; use crate::energy::FunctionBudget; use crate::host::instance_env::InstanceEnv; use crate::host::module_common::run_describer; -use crate::host::wasm_common::module_host_actor::{AnonymousViewOp, DescribeError, InitializationError, ViewOp}; +use crate::host::wasm_common::module_host_actor::{ + AnonymousViewOp, DescribeError, ExecutionStats, InitializationError, ViewOp, +}; use crate::host::wasm_common::*; -use crate::host::wasmtime::wasm_instance_env::FuncCallType; use crate::util::string_from_utf8_lossy_owned; use futures_util::FutureExt; -use spacetimedb_datastore::locking_tx_datastore::ViewCall; +use spacetimedb_datastore::locking_tx_datastore::{FuncCallType, ViewCall}; use spacetimedb_lib::{ConnectionId, Identity}; use spacetimedb_primitives::errno::HOST_CALL_FAILURE; use wasmtime::{ @@ -356,7 +357,7 @@ impl module_host_actor::WasmInstance for WasmtimeInstance { } #[tracing::instrument(level = "trace", skip_all)] - fn call_reducer(&mut self, op: ReducerOp<'_>, budget: FunctionBudget) -> module_host_actor::ExecuteResult { + fn call_reducer(&mut self, op: ReducerOp<'_>, budget: FunctionBudget) -> module_host_actor::ReducerExecuteResult { let store = &mut self.store; prepare_store_for_call(store, budget); @@ -397,16 +398,8 @@ impl module_host_actor::WasmInstance for WasmtimeInstance { let call_result = call_result.map(|code| handle_error_sink_code(code, error)); - // Compute fuel and heap usage. - let remaining_fuel = get_store_fuel(store); - let remaining: FunctionBudget = remaining_fuel.into(); - let energy = module_host_actor::EnergyStats { budget, remaining }; - let memory_allocation = store.data().get_mem().memory.data_size(&store); - - module_host_actor::ExecuteResult { - energy, - timings, - memory_allocation, + module_host_actor::ReducerExecuteResult { + stats: get_execution_stats(store, budget, timings), call_result, } } @@ -429,9 +422,7 @@ impl module_host_actor::WasmInstance for WasmtimeInstance { let Some(call_view) = self.call_view.as_ref() else { return module_host_actor::ViewExecuteResult { - energy: module_host_actor::EnergyStats::ZERO, - timings: module_host_actor::ExecutionTimings::zero(), - memory_allocation: get_memory_size(store), + stats: zero_execution_stats(store), call_result: Err(anyhow::anyhow!( "Module defines view {} but does not export `{}`", op.name, @@ -463,16 +454,8 @@ impl module_host_actor::WasmInstance for WasmtimeInstance { .and_then(|code| handle_result_sink_code(code, result_bytes).map_err(|e| anyhow::anyhow!(e))) .map(|r| r.into()); - // Compute fuel and heap usage. - let remaining_fuel = get_store_fuel(store); - let remaining: FunctionBudget = remaining_fuel.into(); - let energy = module_host_actor::EnergyStats { budget, remaining }; - let memory_allocation = store.data().get_mem().memory.data_size(&store); - module_host_actor::ViewExecuteResult { - energy, - timings, - memory_allocation, + stats: get_execution_stats(store, budget, timings), call_result, } } @@ -496,9 +479,7 @@ impl module_host_actor::WasmInstance for WasmtimeInstance { let Some(call_view_anon) = self.call_view_anon.as_ref() else { return module_host_actor::ViewExecuteResult { - energy: module_host_actor::EnergyStats::ZERO, - timings: module_host_actor::ExecutionTimings::zero(), - memory_allocation: get_memory_size(store), + stats: zero_execution_stats(store), call_result: Err(anyhow::anyhow!( "Module defines anonymous view {} but does not export `{}`", op.name, @@ -517,16 +498,9 @@ impl module_host_actor::WasmInstance for WasmtimeInstance { let call_result = call_result .and_then(|code| handle_result_sink_code(code, result_bytes).map_err(|e| anyhow::anyhow!(e))) .map(|r| r.into()); - // Compute fuel and heap usage. - let remaining_fuel = get_store_fuel(store); - let remaining: FunctionBudget = remaining_fuel.into(); - let energy = module_host_actor::EnergyStats { budget, remaining }; - let memory_allocation = store.data().get_mem().memory.data_size(&store); module_host_actor::ViewExecuteResult { - energy, - timings, - memory_allocation, + stats: get_execution_stats(store, budget, timings), call_result, } } @@ -556,9 +530,7 @@ impl module_host_actor::WasmInstance for WasmtimeInstance { let Some(call_procedure) = self.call_procedure.as_ref() else { return module_host_actor::ProcedureExecuteResult { - energy: module_host_actor::EnergyStats::ZERO, - timings: module_host_actor::ExecutionTimings::zero(), - memory_allocation: get_memory_size(store), + stats: zero_execution_stats(store), call_result: Err(anyhow::anyhow!( "Module defines procedure {} but does not export `{}`", op.name, @@ -595,16 +567,8 @@ impl module_host_actor::WasmInstance for WasmtimeInstance { }) }); - let remaining_fuel = get_store_fuel(store); - let remaining = FunctionBudget::from(remaining_fuel); - - let energy = module_host_actor::EnergyStats { budget, remaining }; - let memory_allocation = get_memory_size(store); - module_host_actor::ProcedureExecuteResult { - energy, - timings, - memory_allocation, + stats: get_execution_stats(store, budget, timings), call_result, } } @@ -656,6 +620,33 @@ fn prepare_connection_id_for_call(caller_connection_id: ConnectionId) -> [u64; 2 bytemuck::must_cast(caller_connection_id.as_le_byte_array()) } +/// Compute fuel and heap usage for a call and construct the `ExecutionStats`. +fn get_execution_stats( + store: &Store, + initial_budget: FunctionBudget, + timings: module_host_actor::ExecutionTimings, +) -> ExecutionStats { + let remaining_fuel = get_store_fuel(store); + let remaining: FunctionBudget = remaining_fuel.into(); + let energy = module_host_actor::EnergyStats { + budget: initial_budget, + remaining, + }; + ExecutionStats { + energy, + timings, + memory_allocation: get_memory_size(store), + } +} + +fn zero_execution_stats(store: &Store) -> ExecutionStats { + ExecutionStats { + energy: module_host_actor::EnergyStats::ZERO, + timings: module_host_actor::ExecutionTimings::zero(), + memory_allocation: get_memory_size(store), + } +} + fn get_memory_size(store: &Store) -> usize { store.data().get_mem().memory.data_size(store) } diff --git a/crates/datastore/src/locking_tx_datastore/mod.rs b/crates/datastore/src/locking_tx_datastore/mod.rs index a22bbd4ff16..e66f3dd0998 100644 --- a/crates/datastore/src/locking_tx_datastore/mod.rs +++ b/crates/datastore/src/locking_tx_datastore/mod.rs @@ -3,7 +3,7 @@ pub mod committed_state; pub mod datastore; mod mut_tx; -pub use mut_tx::{MutTxId, ViewCall}; +pub use mut_tx::{FuncCallType, MutTxId, ViewCall}; mod sequence; pub mod state_view; pub use state_view::{IterByColEqTx, IterByColRangeTx}; diff --git a/crates/datastore/src/locking_tx_datastore/mut_tx.rs b/crates/datastore/src/locking_tx_datastore/mut_tx.rs index 9efa373bc8b..7bfa40a8e27 100644 --- a/crates/datastore/src/locking_tx_datastore/mut_tx.rs +++ b/crates/datastore/src/locking_tx_datastore/mut_tx.rs @@ -149,6 +149,14 @@ impl ViewCall { } } +#[derive(Clone, Debug)] +/// The type of operation being performed against the datastore. +pub enum FuncCallType { + Reducer, + Procedure, + View(ViewCall), +} + pub type ViewReadSets = HashMap; /// Represents a Mutable transaction. Holds locks for its duration @@ -173,24 +181,31 @@ static_assert_size!(MutTxId, 448); impl MutTxId { /// Record that a view performs a table scan in this transaction's read set - pub fn record_table_scan(&mut self, view: Option, table_id: TableId) { - if let Some(view) = view { - self.read_sets.entry(view).or_default().insert_table_scan(table_id) + pub fn record_table_scan(&mut self, op: &FuncCallType, table_id: TableId) { + if let FuncCallType::View(view) = op { + self.read_sets + // TODO: change `read_sets` to the use the `HashMap` from `spacetimedb_data_structures` + // and use `entry_ref()` here + .entry(view.clone()) + .or_default() + .insert_table_scan(table_id) } } /// Record that a view performs an index scan in this transaction's read set pub fn record_index_scan( &mut self, - view: Option, + op: &FuncCallType, table_id: TableId, index_id: IndexId, lower: Bound, upper: Bound, ) { - if let Some(view) = view { + if let FuncCallType::View(view) = op { self.read_sets - .entry(view) + // TODO: change `read_sets` to the use the `HashMap` from `spacetimedb_data_structures + // and use `entry_ref()` here + .entry(view.clone()) .or_default() .insert_index_scan(table_id, index_id, lower, upper) } diff --git a/eslint.config.js b/eslint.config.js index 3ca05584ff7..4f96b16ab8e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -76,6 +76,7 @@ export default tseslint.config( ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], "eslint-comments/no-unused-disable": "off", + "@typescript-eslint/no-empty-object-type": ['error', { allowObjectTypes: 'always' }], }, } ); \ No newline at end of file diff --git a/modules/module-test-ts/src/index.ts b/modules/module-test-ts/src/index.ts index ab035e4ebb2..a72225cfa84 100644 --- a/modules/module-test-ts/src/index.ts +++ b/modules/module-test-ts/src/index.ts @@ -137,11 +137,11 @@ const hasSpecialStuffRow = { }; // Rust: two tables with the same row type: player & logged_out_player -const playerLikeRow = { +const playerLikeRow = t.row({ identity: t.identity().primaryKey(), player_id: t.u64().autoInc().unique(), name: t.string().unique(), -}; +}); // ───────────────────────────────────────────────────────────────────────────── // SCHEMA (tables + indexes + visibility) @@ -214,6 +214,17 @@ const spacetimedb = schema( table({ name: 'logged_out_player', public: true }, playerLikeRow) ); +// ───────────────────────────────────────────────────────────────────────────── +// VIEWS +// ───────────────────────────────────────────────────────────────────────────── + +spacetimedb.view( + 'my_player', + playerLikeRow.optional(), + // FIXME: this should not be necessary; change `OptionBuilder` to accept `null|undefined` for `none` + ctx => ctx.db.player.identity.find(ctx.sender) ?? undefined +); + // ───────────────────────────────────────────────────────────────────────────── // REDUCERS (mirroring Rust order & behavior) // ───────────────────────────────────────────────────────────────────────────── @@ -320,7 +331,7 @@ spacetimedb.reducer( // try_insert TestE { id: 0, name: "Tyler" } try { - const inserted = ctx.db.test_e.tryInsert({ id: 0n, name: 'Tyler' }); + const inserted = ctx.db.test_e.insert({ id: 0n, name: 'Tyler' }); console.info(`Inserted: ${JSON.stringify(inserted)}`); } catch (err) { console.info(`Error: ${String(err)}`);