Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 206 additions & 12 deletions crates/bindings-typescript/src/server/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -253,7 +254,7 @@ export const ReducerCtxImpl = class ReducerCtx<
timestamp: Timestamp,
connectionId: ConnectionId | null,
dbView: DbView<any>,
asViews: object = {}
asViews: object = EMPTY_ALIAS_VIEWS
) {
Object.seal(this);
this.sender = sender;
Expand All @@ -263,26 +264,40 @@ export const ReducerCtxImpl = class ReducerCtx<
this.as = asViews as AliasViews<SchemaDef>;
}

/** 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<typeof this>,
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<typeof this>,
sender: Identity,
timestamp: Timestamp,
connectionId: ConnectionId | null,
dbView?: DbView<any>,
asViews?: object
dbView: DbView<any>,
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<any>;
}
me.db = dbView;
me.as = asViews as AliasViews<any>;
}

get databaseIdentity() {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -579,7 +596,7 @@ class ModuleHooksImpl implements ModuleHooks {
}

const ctx = this.#reducerCtx;
ReducerCtxImpl.reset(
ReducerCtxImpl.resetForNamespace(
ctx,
senderIdentity,
new Timestamp(timestamp),
Expand Down Expand Up @@ -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<any> | undefined;
#reducerArgsDeserializers;
#reducerCtx_: InstanceType<typeof ReducerCtxImpl> | 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<any> = 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<any> = 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());

Expand Down
Loading