Skip to content

Commit

Permalink
Typed Events (#418)
Browse files Browse the repository at this point in the history
* type gen completed

* more type generations

* first implimentation, tested with simple events

* cleanup unused handlbars helpers

* fix whitespace

* Update templates/base/api/types.ts.hbs

* Update templates/base/api/types.ts.hbs

* Update templates/base/api/types.ts.hbs

* Update templates/base/client/.hathora/client.ts.hbs

* Update src/generate.ts

* requested pr changes

* Update templates/base/api/types.ts.hbs

* Update templates/base/api/types.ts.hbs

* Update templates/base/api/types.ts.hbs

* Update templates/base/api/types.ts.hbs

* Update templates/base/api/types.ts.hbs

* Update templates/base/api/types.ts.hbs

* Update templates/base/api/types.ts.hbs

* Update templates/base/api/base.ts.hbs

* Apply suggestions from code review

Co-authored-by: Ian Senne <ianssenne@gmail.com>
Co-authored-by: Harsh Pandey <hpandey793@gmail.com>
  • Loading branch information
3 people authored Oct 12, 2022
1 parent 953049c commit 3715bda
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 15 deletions.
9 changes: 9 additions & 0 deletions src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const HathoraConfig = z
initializeArgs: z.optional(z.string()),
error: z.string(),
tick: z.optional(z.number().int().gte(25)),
events: z.optional(z.record(z.string())),
})
.strict();

Expand Down Expand Up @@ -173,6 +174,14 @@ function enrichDoc(doc: z.infer<typeof HathoraConfig>, plugins: string[], appNam
error: getArgsInfo(doc, plugins, doc.error, false),
plugins,
appName,
events:
doc.events === undefined
? {}
: Object.fromEntries(
Object.entries(doc.events).map(([key, val]) => {
return [key, getArgsInfo(doc, plugins, val, false)];
})
),
};
}

Expand Down
10 changes: 7 additions & 3 deletions templates/base/api/base.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@ export const Response: { ok: () => OkResponse; error: (error: {{> renderTypeArg
};

export type ResponseMessage = { type: "response"; msgId: number; response: Response };
export type EventMessage = { type: "event"; event: string };
export type EventMessage = {
type: "event";
event: T.HathoraEventTypes;
data: T.HathoraEventPayloads[T.HathoraEventTypes];
};
export type Message = ResponseMessage | EventMessage;
export const Message: {
response: (msgId: number, response: Response) => ResponseMessage;
event: (event: string) => EventMessage;
event: (event: T.HathoraEventTypes, data: T.HathoraEventPayloads[T.HathoraEventTypes]) => EventMessage;
} = {
response: (msgId, response) => ({ type: "response", msgId, response }),
event: (event) => ({ type: "event", event }),
event: (event, data) => ({ type: "event", event, data }),
};

{{#each auth}}
Expand Down
45 changes: 42 additions & 3 deletions templates/base/api/types.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ import {
EventMessage as _EventMessage,
} from "./base";

{{#if events}}
export enum HathoraEventTypes {
{{#each events}}
{{@key}},
{{/each}}
}
export type HathoraEventPayloads = {
{{#each events}}
[HathoraEventTypes.{{@key}}]: {{> renderTypeArg this}};
{{/each}}
};
{{/if}}
export type HathoraEvents = {{#if events}}{{#each events}}
| { type: HathoraEventTypes.{{@key}}; val: HathoraEventPayloads[HathoraEventTypes.{{@key}}] }{{/each}}{{else}}never{{/if}};
{{#each types}}
{{> renderExportArg}}
{{/each}}
Expand Down Expand Up @@ -398,6 +412,31 @@ export function encodeStateSnapshot(x: {{userState}}) {
{{/with}}
return buf.toBuffer();
}
function encodeEvent(buf: _Writer, event: _EventMessage) {
buf.writeUVarint(event.event);
switch (event.event) {
{{#each events}}
case HathoraEventTypes.{{@key}}: {
const x = event.data as HathoraEventPayloads[HathoraEventTypes.{{@key}}];
{{> renderEncodeArg}};
break;
}
{{/each}}
}
}
function decodeEvent(sb: _Reader): HathoraEvents {
const event = sb.readUVarint();
switch (event) {
{{#each events}}
case HathoraEventTypes.{{@key}}: {
const x = {{> renderDecodeArg}};
return { type: HathoraEventTypes.{{@key}}, val: x };
}
{{/each}}
default:
throw new Error("Unknown event type");
}
}
export function encodeStateUpdate(
x: _DeepPartial<{{userState}}> | undefined,
changedAtDiff: number,
Expand All @@ -414,7 +453,7 @@ export function encodeStateUpdate(
});
const events = messages.flatMap((msg) => (msg.type === "event" ? msg : []));
buf.writeUVarint(events.length);
events.forEach(({ event }) => buf.writeString(event));
events.forEach((message) => encodeEvent(buf, message));
if (x !== undefined) {
{{#with (lookup types userState)}}{{> renderEncodeDiffArg}};{{/with}}
}
Expand All @@ -429,7 +468,7 @@ export function decodeStateUpdate(buf: ArrayBufferView | _Reader): {
stateDiff?: _DeepPartial<{{userState}}>;
changedAtDiff: number;
responses: _ResponseMessage[];
events: _EventMessage[];
events: HathoraEvents[];
} {
const sb = ArrayBuffer.isView(buf) ? new _Reader(buf) : buf;
const changedAtDiff = sb.readUVarint();
Expand All @@ -438,7 +477,7 @@ export function decodeStateUpdate(buf: ArrayBufferView | _Reader): {
const maybeError = parseOptional(sb, () => {{> renderDecodeArg error}});
return _Message.response(msgId, maybeError === undefined ? _Response.ok() : _Response.error(maybeError));
});
const events = [...Array(sb.readUVarint())].map(() => _Message.event(sb.readString()));
const events = [...Array(sb.readUVarint())].map(() => decodeEvent(sb));
const stateDiff = sb.remaining() ? {{#with (lookup types userState)}}{{> renderDecodeDiffArg}}{{/with}} : undefined;
return { stateDiff, changedAtDiff, responses, events };
}
Expand Down
5 changes: 3 additions & 2 deletions templates/base/client/.hathora/client.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import {
{{#each methods}}
{{makeRequestName @key}},
{{/each}}
HathoraEvents,
} from "../../api/types";
import { UserData, Response, Method, COORDINATOR_HOST, MATCHMAKER_HOST } from "../../api/base";

import { computePatch } from "./patch";
import { ConnectionFailure, transformCoordinatorFailure } from "./failures";

export type StateId = string;
export type UpdateArgs = { stateId: StateId; state: UserState; updatedAt: number; events: string[] };
export type UpdateArgs = { stateId: StateId; state: UserState; updatedAt: number; events: HathoraEvents[] };
export type UpdateCallback = (updateArgs: UpdateArgs) => void;
export type ErrorCallback = (error: ConnectionFailure) => void;

Expand Down Expand Up @@ -208,7 +209,7 @@ export class HathoraConnection {
stateId: this.stateId,
state: JSON.parse(JSON.stringify(this.internalState)),
updatedAt: this.changedAt,
events: events.map((e) => e.event),
events,
})
);
responses.forEach(({ msgId, response }) => {
Expand Down
14 changes: 11 additions & 3 deletions templates/base/server/.hathora/methods.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@ import {
{{#each methods}}
{{makeRequestName @key}},
{{/each}}
HathoraEventTypes,
HathoraEventPayloads,
} from "../../api/types";

export interface Context {
chance: ReturnType<typeof Chance>;
time: number;
sendEvent: (event: string, to: UserId) => void;
broadcastEvent: (event: string) => void;
sendEvent: <EventType extends HathoraEventTypes>(
event: EventType,
data: HathoraEventPayloads[EventType],
to: UserId
) => void;
broadcastEvent: <EventType extends HathoraEventTypes>(
event: EventType,
data: HathoraEventPayloads[EventType]
) => void;
}

export interface Methods<T> {
initialize(ctx: Context, request: IInitializeRequest): T;
{{#each methods}}
Expand Down
14 changes: 10 additions & 4 deletions templates/base/server/.hathora/store.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
encodeStateUpdate,
{{userState}} as UserState,
encodeStateError,
HathoraEventTypes,
HathoraEventPayloads,
} from "../../api/types";
import { ImplWrapper } from "./wrapper";
import { computeDiff } from "./diff";
Expand Down Expand Up @@ -250,19 +252,23 @@ function ctx(chance: Chance.Chance, time: number, stateId: RoomId): Context {
return {
chance,
time,
sendEvent(event: string, userId: UserId) {
sendEvent<EventType extends HathoraEventTypes>(
event: EventType,
data: HathoraEventPayloads[EventType],
userId: UserId
) {
const userInfo = stateInfo.get(stateId)?.subscriptions.get(userId);
if (userInfo !== undefined) {
userInfo.messages.push(Message.event(event));
userInfo.messages.push(Message.event(event, data));
if (!changedStates.has(stateId)) {
addPendingMessage(stateId, userId);
}
}
},
broadcastEvent(event: string) {
broadcastEvent<EventType extends HathoraEventTypes>(event: EventType, data: HathoraEventPayloads[EventType]) {
if (stateInfo.has(stateId)) {
for (const [userId, { messages }] of stateInfo.get(stateId)!.subscriptions) {
messages.push(Message.event(event));
messages.push(Message.event(event, data));
if (!changedStates.has(stateId)) {
addPendingMessage(stateId, userId);
}
Expand Down

0 comments on commit 3715bda

Please sign in to comment.