Skip to content

Commit 5ee3de7

Browse files
committed
Add mount function calls in typescript
1 parent 6c9b102 commit 5ee3de7

3 files changed

Lines changed: 196 additions & 14 deletions

File tree

crates/bindings-typescript/src/server/runtime.ts

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import {
1111
RawModuleDef,
1212
ViewResultHeader,
13+
type RawReducerDefV10,
1314
type RawTableDefV10,
1415
type Typespace,
1516
} from '../lib/autogen/types';
@@ -27,6 +28,7 @@ import {
2728
type UniqueIndex,
2829
} from '../lib/indexes';
2930
import { callProcedure } from './procedures';
31+
import type { Reducers } from './reducers';
3032
import {
3133
type AuthCtx,
3234
type JsonObject,
@@ -42,7 +44,7 @@ import type { DbView } from './db_view';
4244
import { getErrorConstructor, SenderError } from './errors';
4345
import { Range, type Bound } from './range';
4446
import { makeRandom, type Random } from './rng';
45-
import type { SchemaInner } from './schema';
47+
import type { MountedDispatchInfo, SchemaInner } from './schema';
4648

4749
const { freeze } = Object;
4850

@@ -212,13 +214,17 @@ export const ReducerCtxImpl = class ReducerCtx<
212214
me: InstanceType<typeof this>,
213215
sender: Identity,
214216
timestamp: Timestamp,
215-
connectionId: ConnectionId | null
217+
connectionId: ConnectionId | null,
218+
dbView?: DbView<any>
216219
) {
217220
me.sender = sender;
218221
me.timestamp = timestamp;
219222
me.connectionId = connectionId;
220223
me.#uuidCounter = undefined;
221224
me.#senderAuth = undefined;
225+
if (dbView !== undefined) {
226+
me.db = dbView;
227+
}
222228
}
223229

224230
get databaseIdentity() {
@@ -272,21 +278,55 @@ export const callUserFunction = function __spacetimedb_end_short_backtrace<
272278
return fn(...args);
273279
};
274280

281+
type FlatMountDispatch = {
282+
reducerFns: Reducers;
283+
reducerDefs: RawReducerDefV10[];
284+
tables: Array<{ accessorName: string; tableDef: RawTableDefV10 }>;
285+
typespace: Typespace;
286+
dbView_: DbView<any> | undefined;
287+
};
288+
289+
function flattenMountDispatches(
290+
dispatches: MountedDispatchInfo[]
291+
): FlatMountDispatch[] {
292+
const result: FlatMountDispatch[] = [];
293+
for (const d of dispatches) {
294+
result.push({
295+
reducerFns: d.reducerFns,
296+
reducerDefs: d.reducerDefs,
297+
tables: d.tables,
298+
typespace: d.typespace,
299+
dbView_: undefined,
300+
});
301+
result.push(...flattenMountDispatches(d.subDispatches));
302+
}
303+
return result;
304+
}
305+
275306
export const makeHooks = (schema: SchemaInner): ModuleHooks =>
276307
new ModuleHooksImpl(schema);
277308

278309
class ModuleHooksImpl implements ModuleHooks {
279310
#schema: SchemaInner;
280311
#dbView_: DbView<any> | undefined;
281312
#reducerArgsDeserializers;
282-
/** Cache the `ReducerCtx` object to avoid allocating anew for ever reducer call. */
313+
#consumerReducerCount: number;
314+
#flatMounts: FlatMountDispatch[];
315+
/** Cache the `ReducerCtx` object to avoid allocating anew for every reducer call. */
283316
#reducerCtx_: InstanceType<typeof ReducerCtxImpl> | undefined;
284317

285318
constructor(schema: SchemaInner) {
286319
this.#schema = schema;
287-
this.#reducerArgsDeserializers = schema.moduleDef.reducers.map(
320+
this.#consumerReducerCount = schema.reducers.length;
321+
this.#flatMounts = flattenMountDispatches(schema.mountedDispatchInfos);
322+
323+
const consumerDeserializers = schema.moduleDef.reducers.map(
288324
({ params }) => ProductType.makeDeserializer(params, schema.typespace)
289325
);
326+
const mountedDeserializers = this.#flatMounts.flatMap(({ reducerDefs, typespace }) =>
327+
reducerDefs.map(({ params }) => ProductType.makeDeserializer(params, typespace))
328+
);
329+
this.#reducerArgsDeserializers = [...consumerDeserializers, ...mountedDeserializers];
290330
}
291331

292332
get #dbView() {
@@ -300,6 +340,18 @@ class ModuleHooksImpl implements ModuleHooks {
300340
));
301341
}
302342

343+
#getMountDbView(mountIdx: number): DbView<any> {
344+
const m = this.#flatMounts[mountIdx];
345+
return (m.dbView_ ??= freeze(
346+
Object.fromEntries(
347+
m.tables.map(({ accessorName, tableDef }) => [
348+
accessorName,
349+
makeTableView(m.typespace, tableDef),
350+
])
351+
)
352+
));
353+
}
354+
303355
get #reducerCtx() {
304356
return (this.#reducerCtx_ ??= new ReducerCtxImpl(
305357
Identity.zero(),
@@ -333,19 +385,42 @@ class ModuleHooksImpl implements ModuleHooks {
333385
timestamp: bigint,
334386
argsBuf: DataView
335387
): void {
336-
const moduleCtx = this.#schema;
337388
const deserializeArgs = this.#reducerArgsDeserializers[reducerId];
338389
BINARY_READER.reset(argsBuf);
339390
const args = deserializeArgs(BINARY_READER);
340391
const senderIdentity = new Identity(sender);
392+
393+
let fn: ((...args: any[]) => any) | undefined;
394+
let dbView: DbView<any>;
395+
396+
if (reducerId < this.#consumerReducerCount) {
397+
fn = this.#schema.reducers[reducerId];
398+
dbView = this.#dbView;
399+
} else {
400+
let offset = this.#consumerReducerCount;
401+
for (let i = 0; i < this.#flatMounts.length; i++) {
402+
const m = this.#flatMounts[i];
403+
if (reducerId < offset + m.reducerFns.length) {
404+
fn = m.reducerFns[reducerId - offset];
405+
dbView = this.#getMountDbView(i);
406+
break;
407+
}
408+
offset += m.reducerFns.length;
409+
}
410+
if (fn === undefined) {
411+
throw new RangeError(`unknown reducerId ${reducerId}`);
412+
}
413+
}
414+
341415
const ctx = this.#reducerCtx;
342416
ReducerCtxImpl.reset(
343417
ctx,
344418
senderIdentity,
345419
new Timestamp(timestamp),
346-
ConnectionId.nullIfZero(new ConnectionId(connId))
420+
ConnectionId.nullIfZero(new ConnectionId(connId)),
421+
dbView!
347422
);
348-
callUserFunction(moduleCtx.reducers[reducerId], ctx, args);
423+
callUserFunction(fn, ctx, args);
349424
}
350425

351426
__call_view__(

crates/bindings-typescript/src/server/schema.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { moduleHooks, type ModuleDefaultExport } from 'spacetime:sys@2.0';
22
import { CaseConversionPolicy, Lifecycle } from '../lib/autogen/types';
3-
import type { RawModuleDefV10 } from '../lib/autogen/types';
3+
import type {
4+
RawModuleDefV10,
5+
RawReducerDefV10,
6+
RawTableDefV10,
7+
Typespace,
8+
} from '../lib/autogen/types';
49
import {
510
type ParamsAsObject,
611
type ParamsObj,
@@ -44,6 +49,14 @@ import {
4449
} from './views';
4550
import type { UntypedTableDef } from '../lib/table';
4651

52+
export type MountedDispatchInfo = {
53+
reducerFns: Reducers;
54+
reducerDefs: RawReducerDefV10[];
55+
typespace: Typespace;
56+
tables: Array<{ accessorName: string; tableDef: RawTableDefV10 }>;
57+
subDispatches: MountedDispatchInfo[];
58+
};
59+
4760
export class SchemaInner<
4861
S extends UntypedSchemaDef = UntypedSchemaDef,
4962
> extends ModuleContext {
@@ -65,6 +78,7 @@ export class SchemaInner<
6578
string
6679
> = new Map();
6780
pendingSchedules: PendingSchedule[] = [];
81+
mountedDispatchInfos: MountedDispatchInfo[] = [];
6882

6983
constructor(getSchemaType: (ctx: SchemaInner<S>) => S) {
7084
super();
@@ -162,6 +176,10 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
162176
return this.#ctx.typespace;
163177
}
164178

179+
get mountedDispatchInfos(): MountedDispatchInfo[] {
180+
return this.#ctx.mountedDispatchInfos;
181+
}
182+
165183
/** Internal: register exports and materialize the RawModuleDefV10 for upload. */
166184
buildRawModuleDefV10(
167185
exports: object,
@@ -174,6 +192,32 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
174192
return this.#ctx.rawModuleDefV10();
175193
}
176194

195+
/**
196+
* @internal – called by schema() when processing a mounted namespace entry.
197+
* Registers the library's exports and returns both the serialized module def
198+
* and the runtime dispatch info needed by ModuleHooksImpl for __call_reducer__.
199+
*/
200+
buildMountForDispatch(
201+
exports: object
202+
): { rawDef: RawModuleDefV10; dispatch: MountedDispatchInfo } {
203+
const rawDef = this.buildRawModuleDefV10(exports, {
204+
ignoreNonModuleExports: true,
205+
});
206+
return {
207+
rawDef,
208+
dispatch: {
209+
reducerFns: [...this.#ctx.reducers],
210+
reducerDefs: [...this.#ctx.moduleDef.reducers],
211+
typespace: this.#ctx.moduleDef.typespace,
212+
tables: Object.values(this.#ctx.schemaType.tables).map(t => ({
213+
accessorName: t.accessorName,
214+
tableDef: t.tableDef,
215+
})),
216+
subDispatches: [...this.#ctx.mountedDispatchInfos],
217+
},
218+
};
219+
}
220+
177221
/**
178222
* Defines a SpacetimeDB reducer function.
179223
*
@@ -617,12 +661,9 @@ export function schema<const H extends Record<string, SchemaEntry>>(
617661
);
618662
}
619663
if (isMountedModuleNamespace(entry)) {
620-
ctx.addMount({
621-
namespace: accName,
622-
module: entry.default.buildRawModuleDefV10(entry, {
623-
ignoreNonModuleExports: true,
624-
}),
625-
});
664+
const { rawDef, dispatch } = entry.default.buildMountForDispatch(entry);
665+
ctx.addMount({ namespace: accName, module: rawDef });
666+
ctx.mountedDispatchInfos.push(dispatch);
626667
continue;
627668
}
628669
if (!isUntypedTableSchema(entry)) {

crates/bindings-typescript/tests/schema_mounts.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,70 @@ describe('schema mounts', () => {
114114
})
115115
).toThrow(/looks like a default import/);
116116
});
117+
118+
it('populates mountedDispatchInfos with reducer fns and table metadata', () => {
119+
const sessions = table(
120+
{ name: 'sessions' },
121+
{ id: t.u64().primaryKey().autoInc() }
122+
);
123+
124+
const authSchema = schema({ sessions });
125+
const cleanExpiredSessions = authSchema.reducer(() => {});
126+
const authLib = { default: authSchema, cleanExpiredSessions };
127+
128+
const players = table({ name: 'players' }, { id: t.u32().primaryKey() });
129+
const consumer = schema({ players, myauth: authLib });
130+
131+
const infos = consumer.mountedDispatchInfos;
132+
expect(infos).toHaveLength(1);
133+
134+
const info = infos[0];
135+
expect(info.reducerFns).toHaveLength(1);
136+
expect(info.reducerDefs).toHaveLength(1);
137+
expect(info.reducerDefs[0].sourceName).toBe('cleanExpiredSessions');
138+
expect(info.tables).toHaveLength(1);
139+
expect(info.tables[0].accessorName).toBe('sessions');
140+
expect(info.subDispatches).toHaveLength(0);
141+
});
142+
143+
it('flattens nested mount dispatches depth-first', () => {
144+
// baz library: 1 reducer
145+
const bazTable = table({ name: 'baz_items' }, { id: t.u32().primaryKey() });
146+
const bazSchema = schema({ bazTable });
147+
const bazReducer = bazSchema.reducer(() => {});
148+
const bazLib = { default: bazSchema, bazReducer };
149+
150+
// auth library: 1 own reducer, mounts baz
151+
const sessions = table(
152+
{ name: 'sessions' },
153+
{ id: t.u64().primaryKey().autoInc() }
154+
);
155+
const authSchema = schema({ sessions, baz: bazLib });
156+
const authReducer = authSchema.reducer(() => {});
157+
const authLib = { default: authSchema, authReducer };
158+
159+
// consumer: 1 own reducer, mounts auth
160+
const players = table({ name: 'players' }, { id: t.u32().primaryKey() });
161+
const consumer = schema({ players, myauth: authLib });
162+
const consumerReducer = consumer.reducer(() => {});
163+
164+
// Verify depth-first structure:
165+
// consumer.mountedDispatchInfos[0] = myauth (authReducer)
166+
// consumer.mountedDispatchInfos[0].subDispatches[0] = myauth.baz (bazReducer)
167+
const infos = consumer.mountedDispatchInfos;
168+
expect(infos).toHaveLength(1);
169+
170+
const authInfo = infos[0];
171+
expect(authInfo.reducerFns).toHaveLength(1);
172+
expect(authInfo.reducerDefs[0].sourceName).toBe('authReducer');
173+
expect(authInfo.subDispatches).toHaveLength(1);
174+
175+
const bazInfo = authInfo.subDispatches[0];
176+
expect(bazInfo.reducerFns).toHaveLength(1);
177+
expect(bazInfo.reducerDefs[0].sourceName).toBe('bazReducer');
178+
expect(bazInfo.subDispatches).toHaveLength(0);
179+
180+
// Unused variable check
181+
void consumerReducer;
182+
});
117183
});

0 commit comments

Comments
 (0)