Skip to content

Instantly share code, notes, and snippets.

@elierotenberg
Last active March 5, 2019 19:17
Show Gist options
  • Save elierotenberg/b20b34372b4e23048a9d9154dd533923 to your computer and use it in GitHub Desktop.
Save elierotenberg/b20b34372b4e23048a9d9154dd533923 to your computer and use it in GitHub Desktop.
Statically typechecked serializable protocol
import { IProtocol, IRequestResponseMap, IEventMap } from "./IProtocol";
import { ISerializable } from "./ISerializable";
type RequestResponseMap = {
ping: {
RequestParams: {};
ResponseParams: {};
};
echo: {
RequestParams: ISerializable;
ResponseParams: ISerializable;
};
authenticate: {
RequestParams: {
username: string;
password: string;
};
ResponseParams: { err: null; token: string } | { err: string; token: null };
};
};
type EventMap = {
tick: { now: number };
};
type IMyProtocol = IProtocol<RequestResponseMap, EventMap>;
export { IMyProtocol };
import { ISerializable } from "./ISerializable";
type IMapKey = number | symbol | string;
type IMessage<
TypeT extends "request" | "response" | "event",
KindT extends IMapKey,
ParamsT extends ISerializable
> = {
readonly type: TypeT;
readonly kind: KindT;
readonly params: ParamsT;
};
type IRequest<KindT extends IMapKey, ParamsT extends ISerializable> = IMessage<
"request",
KindT,
ParamsT
>;
type IResponse<KindT extends IMapKey, ParamsT extends ISerializable> = IMessage<
"response",
KindT,
ParamsT
>;
type IEvent<KindT extends IMapKey, ParamsT extends ISerializable> = IMessage<
"event",
KindT,
ParamsT
>;
type IRequestResponseMap<KindT extends IMapKey> = {
readonly [kind in KindT]: {
readonly RequestParams: IRequest<kind, any>["params"];
readonly ResponseParams: IResponse<kind, any>["params"];
}
};
type IEventMap<KindT extends IMapKey> = {
readonly [kind in KindT]: IEvent<kind, any>["params"]
};
type IProtocol<
RequestResponsePairMapT extends IRequestResponseMap<any>,
EventMapT extends IEventMap<any>
> = {
readonly Request: new <KindT extends keyof RequestResponsePairMapT>(
kind: KindT,
params: RequestResponsePairMapT[KindT]["RequestParams"],
) => IRequest<KindT, RequestResponsePairMapT[KindT]["RequestParams"]>;
readonly Response: new <KindT extends keyof RequestResponsePairMapT>(
kind: KindT,
params: RequestResponsePairMapT[KindT]["ResponseParams"],
) => IResponse<KindT, RequestResponsePairMapT[KindT]["ResponseParams"]>;
readonly Event: new <KindT extends keyof EventMapT>(
kind: KindT,
params: EventMapT[KindT]["params"],
) => IEvent<KindT, EventMapT[KindT]["params"]>;
};
type IServer<ProtocolT extends IProtocol<any, any>> = {
readonly listen: <
RequestT extends InstanceType<ProtocolT["Request"]>,
ResponseT extends InstanceType<ProtocolT["Response"]> &
IResponse<RequestT["kind"], any>
>(
listener: (request: RequestT) => Promise<ResponseT>,
) => () => void;
readonly emit: (event: InstanceType<ProtocolT["Event"]>) => void;
};
type IClient<ProtocolT extends IProtocol<any, any>> = {
readonly subscribe: (
subscriber: (event: InstanceType<ProtocolT["Event"]>) => void,
) => () => void;
readonly fetch: <
RequestT extends InstanceType<ProtocolT["Request"]>,
ResponseT extends InstanceType<ProtocolT["Response"]> &
IResponse<RequestT["kind"], any>
>(
request: RequestT,
) => Promise<ResponseT>;
};
export {
IMapKey,
IMessage,
IRequest,
IResponse,
IEvent,
IRequestResponseMap,
IEventMap,
IProtocol,
IServer,
IClient,
};
type ISerializable =
| string
| number
| boolean
| ISerializableObject
| ISerializableArray;
interface ISerializableObject {
readonly [key: string]: ISerializable;
}
interface ISerializableArray extends Array<ISerializable> {}
interface ISerializer {
readonly serialize: (serializable: ISerializable) => string;
readonly unserialize: (serialized: string) => ISerializable;
}
export { ISerializable, ISerializableObject, ISerializer };
import { IKernelShellProtocol } from "./IKernelShellProtocol";
import { UniversalProtocol } from "./UniversalProtocol";
const MyProtocol: IKernelShellProtocol = UniversalProtocol;
const t1 = new MyProtocol.Request("ping", {}); // passes
const t2 = new MyProtocol.Request("ping", null); // fails at compile time
const t3 = new MyProtocol.Request("authenticate", {
username: "my-username",
}); // fails at compile time (missing property: password)
const t4 = new MyProtocol.Response("authenticate", {
err: "an error has occured",
token: null,
}); // passes
const t5 = new MyProtocol.Response("authenticate", {
err: null,
token: null,
}); // fails at compile time (err and token can't be both null)
import {
IEvent,
IMapKey,
IMessage,
IProtocol,
IRequest,
IResponse,
} from "./IProtocol";
import { ISerializable } from "./ISerializable";
class UniversalMessage<
TypeT extends "request" | "response" | "event",
KindT extends IMapKey,
ParamsT extends ISerializable
> implements IMessage<TypeT, KindT, ParamsT> {
readonly type: TypeT;
readonly kind: KindT;
readonly params: ParamsT;
constructor(type: TypeT, kind: KindT, params: ParamsT) {
this.type = type;
this.kind = kind;
this.params = params;
}
}
class UniversalRequest<KindT extends IMapKey, ParamsT extends ISerializable>
extends UniversalMessage<"request", KindT, ParamsT>
implements IRequest<KindT, ParamsT> {
constructor(kind: KindT, params: ParamsT) {
super("request", kind, params);
}
}
class UniversalResponse<KindT extends IMapKey, ParamsT extends ISerializable>
extends UniversalMessage<"response", KindT, ParamsT>
implements IResponse<KindT, ParamsT> {
constructor(kind: KindT, params: ParamsT) {
super("response", kind, params);
}
}
class UniversalEvent<KindT extends IMapKey, ParamsT extends ISerializable>
extends UniversalMessage<"event", KindT, ParamsT>
implements IEvent<KindT, ParamsT> {
constructor(kind: KindT, params: ParamsT) {
super("event", kind, params);
}
}
const UniversalProtocol: IProtocol<any, any> = {
Request: UniversalRequest,
Response: UniversalResponse,
Event: UniversalEvent,
};
export { UniversalProtocol };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment