Skip to content

Commit 0202453

Browse files
committed
Add ctx.as.<namespace> for mounted submodules in typescript
1 parent 14dd5da commit 0202453

3 files changed

Lines changed: 176 additions & 3 deletions

File tree

crates/bindings-typescript/src/lib/reducers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ export interface JwtClaims {
9898
readonly fullPayload: JsonObject;
9999
}
100100

101+
export type AliasViews<SchemaDef extends UntypedSchemaDef> =
102+
SchemaDef extends { namespaces: infer NS extends Record<string, UntypedSchemaDef> }
103+
? { readonly [K in keyof NS]: ReducerCtx<NS[K]> }
104+
: {};
105+
101106
/**
102107
* Reducer context parametrized by the inferred Schema
103108
*/
@@ -113,4 +118,5 @@ export type ReducerCtx<SchemaDef extends UntypedSchemaDef> = Readonly<{
113118
newUuidV4(): Uuid;
114119
newUuidV7(): Uuid;
115120
random: Random;
121+
as: AliasViews<SchemaDef>;
116122
}>;

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

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
import { callProcedure } from './procedures';
3131
import type { Reducers } from './reducers';
3232
import {
33+
type AliasViews,
3334
type AuthCtx,
3435
type JsonObject,
3536
type JwtClaims,
@@ -195,18 +196,21 @@ export const ReducerCtxImpl = class ReducerCtx<
195196
timestamp: Timestamp;
196197
connectionId: ConnectionId | null;
197198
db: DbView<SchemaDef>;
199+
as: AliasViews<SchemaDef>;
198200

199201
constructor(
200202
sender: Identity,
201203
timestamp: Timestamp,
202204
connectionId: ConnectionId | null,
203-
dbView: DbView<any>
205+
dbView: DbView<any>,
206+
asViews: object = {}
204207
) {
205208
Object.seal(this);
206209
this.sender = sender;
207210
this.timestamp = timestamp;
208211
this.connectionId = connectionId;
209212
this.db = dbView as unknown as DbView<SchemaDef>;
213+
this.as = asViews as AliasViews<SchemaDef>;
210214
}
211215

212216
/** Reset the `ReducerCtx` to be used for a new transaction */
@@ -215,7 +219,8 @@ export const ReducerCtxImpl = class ReducerCtx<
215219
sender: Identity,
216220
timestamp: Timestamp,
217221
connectionId: ConnectionId | null,
218-
dbView?: DbView<any>
222+
dbView?: DbView<any>,
223+
asViews?: object
219224
) {
220225
me.sender = sender;
221226
me.timestamp = timestamp;
@@ -225,6 +230,9 @@ export const ReducerCtxImpl = class ReducerCtx<
225230
if (dbView !== undefined) {
226231
me.db = dbView;
227232
}
233+
if (asViews !== undefined) {
234+
me.as = asViews as AliasViews<any>;
235+
}
228236
}
229237

230238
get databaseIdentity() {
@@ -309,6 +317,7 @@ export const makeHooks = (schema: SchemaInner): ModuleHooks =>
309317
class ModuleHooksImpl implements ModuleHooks {
310318
#schema: SchemaInner;
311319
#dbView_: DbView<any> | undefined;
320+
#consumerAs_: object | undefined;
312321
#reducerArgsDeserializers;
313322
#consumerReducerCount: number;
314323
#flatMounts: FlatMountDispatch[];
@@ -366,6 +375,13 @@ class ModuleHooksImpl implements ModuleHooks {
366375
));
367376
}
368377

378+
get #consumerAs() {
379+
return (this.#consumerAs_ ??= buildAliasCtxMap(
380+
this.#reducerCtx,
381+
this.#schema.mountedDispatchInfos
382+
));
383+
}
384+
369385
__describe_module__() {
370386
const writer = new BinaryWriter(128);
371387
RawModuleDef.serialize(
@@ -397,10 +413,12 @@ class ModuleHooksImpl implements ModuleHooks {
397413

398414
let fn: ((...args: any[]) => any) | undefined;
399415
let dbView: DbView<any>;
416+
let asViews: object;
400417

401418
if (reducerId < this.#consumerReducerCount) {
402419
fn = this.#schema.reducers[reducerId];
403420
dbView = this.#dbView;
421+
asViews = this.#consumerAs;
404422
} else {
405423
let offset = this.#consumerReducerCount;
406424
for (let i = 0; i < this.#flatMounts.length; i++) {
@@ -415,6 +433,7 @@ class ModuleHooksImpl implements ModuleHooks {
415433
if (fn === undefined) {
416434
throw new RangeError(`unknown reducerId ${reducerId}`);
417435
}
436+
asViews = {};
418437
}
419438

420439
const ctx = this.#reducerCtx;
@@ -423,7 +442,8 @@ class ModuleHooksImpl implements ModuleHooks {
423442
senderIdentity,
424443
new Timestamp(timestamp),
425444
ConnectionId.nullIfZero(new ConnectionId(connId)),
426-
dbView!
445+
dbView!,
446+
asViews!
427447
);
428448
callUserFunction(fn, ctx, args);
429449
}
@@ -515,6 +535,36 @@ function buildDbViewForDispatch(dispatch: MountedDispatchInfo): object {
515535
return freeze(Object.fromEntries([...tableEntries, ...subNsEntries]));
516536
}
517537

538+
function buildAliasCtx(
539+
parent: InstanceType<typeof ReducerCtxImpl>,
540+
dispatch: MountedDispatchInfo
541+
): object {
542+
const nsDb = buildDbViewForDispatch(dispatch);
543+
const subAs = buildAliasCtxMap(parent, dispatch.subDispatches);
544+
return {
545+
get sender() { return parent.sender; },
546+
get databaseIdentity() { return parent.databaseIdentity; },
547+
get identity() { return parent.identity; },
548+
get timestamp() { return parent.timestamp; },
549+
get connectionId() { return parent.connectionId; },
550+
get senderAuth() { return parent.senderAuth; },
551+
get random() { return parent.random; },
552+
newUuidV4() { return parent.newUuidV4(); },
553+
newUuidV7() { return parent.newUuidV7(); },
554+
db: nsDb,
555+
as: subAs,
556+
};
557+
}
558+
559+
function buildAliasCtxMap(
560+
parent: InstanceType<typeof ReducerCtxImpl>,
561+
dispatches: MountedDispatchInfo[]
562+
): object {
563+
return freeze(
564+
Object.fromEntries(dispatches.map(d => [d.namespace, buildAliasCtx(parent, d)]))
565+
);
566+
}
567+
518568
function makeTableView(
519569
typespace: Typespace,
520570
table: RawTableDefV10
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { beforeAll, describe, expect, it, vi } from 'vitest';
2+
3+
vi.mock(
4+
'spacetime:sys@2.0',
5+
() => ({
6+
moduleHooks: Symbol('moduleHooks'),
7+
table_id_from_name: () => 1,
8+
index_id_from_name: () => 1,
9+
row_iter_bsatn_close: () => {},
10+
}),
11+
{ virtual: true }
12+
);
13+
14+
vi.mock('spacetime:sys@2.1', () => ({}), { virtual: true });
15+
16+
describe('ctx.as alias proxy', () => {
17+
let schema: typeof import('../src/server/schema').schema;
18+
let table: typeof import('../src/lib/table').table;
19+
let t: typeof import('../src/lib/type_builders').t;
20+
let moduleHooks: symbol;
21+
22+
beforeAll(async () => {
23+
({ schema } = await import('../src/server/schema'));
24+
({ table } = await import('../src/lib/table'));
25+
({ t } = await import('../src/lib/type_builders'));
26+
({ moduleHooks } = (await import('spacetime:sys@2.0')) as any);
27+
});
28+
29+
it('ctx.as.<alias> provides a narrowed ctx with library db and delegating sender', async () => {
30+
const sessions = table(
31+
{ name: 'sessions' },
32+
{ id: t.u64().primaryKey().autoInc() }
33+
);
34+
const authSchema = schema({ sessions });
35+
const authLib = { default: authSchema };
36+
37+
const players = table({ name: 'players' }, { id: t.u32().primaryKey() });
38+
const consumer = schema({ players, myauth: authLib });
39+
40+
let capturedCtx: any;
41+
const myReducer = consumer.reducer((ctx: any) => {
42+
capturedCtx = ctx;
43+
});
44+
45+
const hooks = (consumer as any)[moduleHooks]({ myReducer });
46+
hooks.__call_reducer__(
47+
0,
48+
0n,
49+
0n,
50+
0n,
51+
new DataView(new ArrayBuffer(0))
52+
);
53+
54+
expect(capturedCtx.as).toBeDefined();
55+
expect(capturedCtx.as.myauth).toBeDefined();
56+
expect(capturedCtx.as.myauth.db).toBeDefined();
57+
expect(capturedCtx.as.myauth.db.sessions).toBeDefined();
58+
expect(capturedCtx.as.myauth.sender).toBe(capturedCtx.sender);
59+
expect(capturedCtx.as.myauth.timestamp).toBe(capturedCtx.timestamp);
60+
});
61+
62+
it('ctx.as is empty object when there are no mounts', async () => {
63+
const players = table({ name: 'players' }, { id: t.u32().primaryKey() });
64+
const consumer = schema({ players });
65+
66+
let capturedCtx: any;
67+
const myReducer = consumer.reducer((ctx: any) => {
68+
capturedCtx = ctx;
69+
});
70+
71+
const hooks = (consumer as any)[moduleHooks]({ myReducer });
72+
hooks.__call_reducer__(
73+
0,
74+
0n,
75+
0n,
76+
0n,
77+
new DataView(new ArrayBuffer(0))
78+
);
79+
80+
expect(capturedCtx.as).toBeDefined();
81+
expect(Object.keys(capturedCtx.as)).toHaveLength(0);
82+
});
83+
84+
it('ctx.as.<alias>.as carries nested sub-mount aliases', async () => {
85+
const bazTable = table({ name: 'baz_items' }, { id: t.u32().primaryKey() });
86+
const bazSchema = schema({ bazTable });
87+
const bazLib = { default: bazSchema };
88+
89+
const sessions = table(
90+
{ name: 'sessions' },
91+
{ id: t.u64().primaryKey().autoInc() }
92+
);
93+
const authSchema = schema({ sessions, baz: bazLib });
94+
const authLib = { default: authSchema };
95+
96+
const players = table({ name: 'players' }, { id: t.u32().primaryKey() });
97+
const consumer = schema({ players, myauth: authLib });
98+
99+
let capturedCtx: any;
100+
const myReducer = consumer.reducer((ctx: any) => {
101+
capturedCtx = ctx;
102+
});
103+
104+
const hooks = (consumer as any)[moduleHooks]({ myReducer });
105+
hooks.__call_reducer__(
106+
0,
107+
0n,
108+
0n,
109+
0n,
110+
new DataView(new ArrayBuffer(0))
111+
);
112+
113+
expect(capturedCtx.as.myauth.as.baz).toBeDefined();
114+
expect(capturedCtx.as.myauth.as.baz.db.bazTable).toBeDefined();
115+
expect(capturedCtx.as.myauth.as.baz.sender).toBe(capturedCtx.sender);
116+
});
117+
});

0 commit comments

Comments
 (0)