diff --git a/crates/bindings-typescript/src/server/runtime.ts b/crates/bindings-typescript/src/server/runtime.ts index cf579e87631..d80e7687e07 100644 --- a/crates/bindings-typescript/src/server/runtime.ts +++ b/crates/bindings-typescript/src/server/runtime.ts @@ -78,6 +78,7 @@ import type { SubmoduleDispatchInfo, SchemaInner } from './schema'; import { HttpRequest, HttpResponse } from '../lib/autogen/types'; const { freeze } = Object; +const EMPTY_ALIAS_VIEWS = freeze({}); export const sys = { ..._syscalls2_0, ..._syscalls2_1 }; @@ -253,7 +254,7 @@ export const ReducerCtxImpl = class ReducerCtx< timestamp: Timestamp, connectionId: ConnectionId | null, dbView: DbView, - asViews: object = {} + asViews: object = EMPTY_ALIAS_VIEWS ) { Object.seal(this); this.sender = sender; @@ -263,26 +264,40 @@ export const ReducerCtxImpl = class ReducerCtx< this.as = asViews as AliasViews; } - /** Reset the `ReducerCtx` to be used for a new transaction */ + /** Reset the `ReducerCtx` to be used for a new root-module transaction. */ static reset( + me: InstanceType, + sender: Identity, + timestamp: Timestamp, + connectionId: ConnectionId | null + ) { + me.sender = sender; + me.timestamp = timestamp; + me.connectionId = connectionId; + me.#uuidCounter = undefined; + me.#senderAuth = undefined; + } + + /** + * Reset the reusable `ReducerCtx` for a reducer mounted in a namespace. + * `dbView` and `asViews` select the table and alias views belonging to that + * reducer's module. + */ + static resetForNamespace( me: InstanceType, sender: Identity, timestamp: Timestamp, connectionId: ConnectionId | null, - dbView?: DbView, - asViews?: object + dbView: DbView, + asViews: object ) { me.sender = sender; me.timestamp = timestamp; me.connectionId = connectionId; me.#uuidCounter = undefined; me.#senderAuth = undefined; - if (dbView !== undefined) { - me.db = dbView; - } - if (asViews !== undefined) { - me.as = asViews as AliasViews; - } + me.db = dbView; + me.as = asViews as AliasViews; } get databaseIdentity() { @@ -413,7 +428,9 @@ function flattenSubmoduleDispatches( } export const makeHooks = (schema: SchemaInner): ModuleHooks => - new ModuleHooksImpl(schema); + schema.submoduleDispatchInfos.length === 0 + ? new RootModuleHooksImpl(schema) + : new ModuleHooksImpl(schema); class ModuleHooksImpl implements ModuleHooks { #schema: SchemaInner; @@ -579,7 +596,7 @@ class ModuleHooksImpl implements ModuleHooks { } const ctx = this.#reducerCtx; - ReducerCtxImpl.reset( + ReducerCtxImpl.resetForNamespace( ctx, senderIdentity, new Timestamp(timestamp), @@ -767,6 +784,183 @@ class ModuleHooksImpl implements ModuleHooks { } } +/** + * Hooks for modules without mounted submodules. + * + * This intentionally does not extend `ModuleHooksImpl`. Selecting this class + * in `makeHooks` gives root-only modules the pre-submodules hook-object field + * set and root-only dispatch methods. The only intentional shape difference in + * the cached reducer context is the empty `ctx.as` view required by the current + * API. + */ +class RootModuleHooksImpl implements ModuleHooks { + #schema: SchemaInner; + #dbView_: DbView | undefined; + #reducerArgsDeserializers; + #reducerCtx_: InstanceType | undefined; + + constructor(schema: SchemaInner) { + this.#schema = schema; + this.#reducerArgsDeserializers = schema.moduleDef.reducers.map( + ({ params }) => ProductType.makeDeserializer(params, schema.typespace) + ); + } + + get #dbView() { + return (this.#dbView_ ??= freeze( + Object.fromEntries( + Object.values(this.#schema.schemaType.tables).map(table => [ + table.accessorName, + makeTableView(this.#schema.typespace, table.tableDef), + ]) + ) + )); + } + + get #reducerCtx() { + return (this.#reducerCtx_ ??= new ReducerCtxImpl( + Identity.zero(), + Timestamp.UNIX_EPOCH, + null, + this.#dbView + )); + } + + __describe_module__() { + const writer = new BinaryWriter(128); + RawModuleDef.serialize( + writer, + RawModuleDef.V10(this.#schema.rawModuleDefV10()) + ); + return writer.getBuffer(); + } + + __get_error_constructor__(code: number): new (msg: string) => Error { + return getErrorConstructor(code); + } + + get __sender_error_class__() { + return SenderError; + } + + __call_reducer__( + reducerId: u32, + sender: u256, + connId: u128, + timestamp: bigint, + argsBuf: DataView + ): void { + const moduleCtx = this.#schema; + const deserializeArgs = this.#reducerArgsDeserializers[reducerId]; + BINARY_READER.reset(argsBuf); + const args = deserializeArgs(BINARY_READER); + const senderIdentity = new Identity(sender); + const ctx = this.#reducerCtx; + ReducerCtxImpl.reset( + ctx, + senderIdentity, + new Timestamp(timestamp), + ConnectionId.nullIfZero(new ConnectionId(connId)) + ); + callUserFunction(moduleCtx.reducers[reducerId], ctx, args); + } + + __call_view__( + id: u32, + sender: u256, + argsBuf: Uint8Array + ): { data: Uint8Array } { + const moduleCtx = this.#schema; + const { fn, deserializeParams, serializeReturn, returnTypeBaseSize } = + moduleCtx.views[id]; + const ctx: ViewCtx = freeze({ + sender: new Identity(sender), + // This is the mutable DbView, but the user-facing type is readonly. Any + // attempted mutation still fails at runtime because this is a view call. + db: this.#dbView, + from: makeQueryBuilder(moduleCtx.schemaType), + }); + const args = deserializeParams(new BinaryReader(argsBuf)); + const ret = callUserFunction(fn, ctx, args); + const retBuf = new BinaryWriter(returnTypeBaseSize); + if (isRowTypedQuery(ret)) { + const query = toSql(ret); + ViewResultHeader.serialize(retBuf, ViewResultHeader.RawSql(query)); + } else { + ViewResultHeader.serialize(retBuf, ViewResultHeader.RowData); + serializeReturn(retBuf, ret); + } + return { data: retBuf.getBuffer() }; + } + + __call_view_anon__(id: u32, argsBuf: Uint8Array): { data: Uint8Array } { + const moduleCtx = this.#schema; + const { fn, deserializeParams, serializeReturn, returnTypeBaseSize } = + moduleCtx.anonViews[id]; + const ctx: AnonymousViewCtx = freeze({ + // This is the mutable DbView, but the user-facing type is readonly. Any + // attempted mutation still fails at runtime because this is a view call. + db: this.#dbView, + from: makeQueryBuilder(moduleCtx.schemaType), + }); + const args = deserializeParams(new BinaryReader(argsBuf)); + const ret = callUserFunction(fn, ctx, args); + const retBuf = new BinaryWriter(returnTypeBaseSize); + if (isRowTypedQuery(ret)) { + const query = toSql(ret); + ViewResultHeader.serialize(retBuf, ViewResultHeader.RawSql(query)); + } else { + ViewResultHeader.serialize(retBuf, ViewResultHeader.RowData); + serializeReturn(retBuf, ret); + } + return { data: retBuf.getBuffer() }; + } + + __call_procedure__( + id: u32, + sender: u256, + connection_id: u128, + timestamp: bigint, + args: Uint8Array + ): Uint8Array { + return callProcedure( + this.#schema.procedures, + id, + new Identity(sender), + ConnectionId.nullIfZero(new ConnectionId(connection_id)), + new Timestamp(timestamp), + args, + () => this.#dbView + ); + } + + __call_http_handler__( + id: u32, + timestamp: bigint, + request: Uint8Array, + body: Uint8Array + ): [response: Uint8Array, body: Uint8Array] { + const moduleCtx = this.#schema; + const handler = moduleCtx.httpHandlers[id]; + const ctx = new HandlerContextImpl( + new Timestamp(timestamp), + () => this.#dbView + ); + const requestMetadata = HttpRequest.deserialize(new BinaryReader(request)); + const response = callUserFunction( + handler, + ctx, + requestFromWire(requestMetadata, body) + ); + const [responseMetadata, responseBody] = responseIntoWire(response); + const responseBuf = new BinaryWriter( + bsatnBaseSize(moduleCtx.typespace, HttpResponse.algebraicType) + ); + HttpResponse.serialize(responseBuf, responseMetadata); + return [responseBuf.getBuffer(), responseBody]; + } +} + const BINARY_WRITER = new BinaryWriter(0); const BINARY_READER = new BinaryReader(new Uint8Array());