Skip to content

Instantly share code, notes, and snippets.

@jjbubudi
Last active February 4, 2019 06:26
Show Gist options
  • Save jjbubudi/d293fe437e716151dea9753c9b4d78bb to your computer and use it in GitHub Desktop.
Save jjbubudi/d293fe437e716151dea9753c9b4d78bb to your computer and use it in GitHub Desktop.
type double = number;
type float = number;
type int32 = number;
type int64 = number;
type uint32 = number;
type uint64 = number;
type sint32 = number;
type sint64 = number;
type fixed32 = number;
type fixed64 = number;
type sfixed32 = number;
type sfixed64 = number;
type bytes = Uint8Array;
type ProtobufTypes =
double
| float
| int32
| int64
| uint32
| uint64
| sint32
| sint64
| fixed32
| fixed64
| sfixed32
| sfixed64
| boolean
| string
| bytes
| { [index: number]: ProtobufTypes }
| { [field: string]: ProtobufTypes };
type Encoded = [number, ...number[]];
type Decoded<T> = [T, number];
export class Field<T extends ProtobufTypes> {
constructor(
readonly tag: number,
readonly encode: (data: T) => Encoded,
readonly decode: (offset: number, incoming: Readonly<Uint8Array>) => Decoded<T>
) { }
}
interface Schema {
readonly [key: string]: Field<any>;
}
type FieldType<T extends Field<any>> = T extends Field<infer R> ? R : never;
type SchemaType<T extends CompiledSchema<any>> = T extends CompiledSchema<infer R> ? R : never;
type AsObject<T extends Schema> = { +readonly [K in keyof T]: FieldType<T[K]> };
interface CompiledSchema<S extends Schema> {
field(tag: number): Field<AsObject<S>>;
encode(o: AsObject<S>): Uint8Array;
decode(b: Readonly<Uint8Array>): AsObject<S>;
}
function noopEncode<T>(data: T): Encoded {
return [0];
}
function decodeInt32(offset: number, incoming: Readonly<Uint8Array>): Decoded<int32> {
const result = decodeUint32(offset, incoming);
result[0] = result[0] | 0;
return result;
}
function decodeUint32(offset: number, incoming: Readonly<Uint8Array>): Decoded<uint32> {
let pos = offset;
let result = 0;
let shift = 0;
let bits: number;
let numberOfBytes = 0;
do {
bits = incoming[pos++];
result |= (bits & 0x7F) << shift;
shift += 7;
numberOfBytes++;
} while ((bits & 0x80) !== 0);
return [result >>> 0, numberOfBytes];
}
export function uint32Field(tag: number): Field<uint32> {
return new Field<uint32>(
tag,
noopEncode,
decodeUint32
);
}
export function int32Field(tag: number): Field<int32> {
return new Field<int32>(
tag,
noopEncode,
decodeInt32
);
}
export function stringField(tag: number): Field<string> {
return new Field<string>(
tag,
noopEncode,
(offset, incoming) => {
return ['hello', 5 + 1];
}
);
}
function decodeBoolean(offset: number, incoming: Readonly<Uint8Array>): Decoded<boolean> {
const result = decodeUint32(offset, incoming)[0] === 1 ? true : false;
return [result, 1];
}
export function booleanField(tag: number): Field<boolean> {
return new Field<boolean>(
tag,
noopEncode,
decodeBoolean
);
}
export function repeated<T extends ProtobufTypes>(field: Field<T>): Field<T[]> {
const decode = field.decode;
return new Field<T[]>(
field.tag,
noopEncode,
(offset, incoming) => {
const size = incoming[offset];
const results = [];
let cursor = 0;
while (cursor < size) {
const [data, length] = decode(cursor + offset + 1, incoming);
results.push(data);
cursor += length;
}
return [results, size + 1];
}
);
}
export function protobufSchema<S extends Schema>(schema: S): CompiledSchema<S> {
return new Serdes(schema);
}
export class Serdes<S extends Schema> implements CompiledSchema<S> {
private readonly tagToDecoder: Readonly<{ [tag: number]: (offset: number, incoming: Readonly<Uint8Array>) => Decoded<any> }>;
private readonly tagToKey: Readonly<{ [tag: number]: string }>;
constructor(schema: S) {
this.tagToDecoder = (() => {
const fields: { [index: number]: (offset: number, incoming: Readonly<Uint8Array>) => Decoded<any> } = {};
for (const k in schema) {
if (!schema.hasOwnProperty(k)) {
continue;
}
fields[schema[k].tag] = schema[k].decode;
}
return fields;
})();
this.tagToKey = (() => {
const tagToKey: { [index: number]: string } = {};
for (const k in schema) {
if (!schema.hasOwnProperty(k)) {
continue;
}
tagToKey[schema[k].tag] = k;
}
return tagToKey;
})();
}
field(tag: number): Field<AsObject<S>> {
const decode = this.decodeDelimited.bind(this);
return new Field(
tag,
noopEncode,
decode
);
}
encode(o: AsObject<S>): Uint8Array {
return new Uint8Array(0);
}
decodeDelimited(offset: number, b: Readonly<Uint8Array>): Decoded<AsObject<S>> {
const finalObject: { [index: string]: any } = {};
const isDelimited = offset > 0;
let messageLength: number;
let sizeLength: number;
if (isDelimited) {
const d = decodeUint32(offset, b);
messageLength = d[0];
sizeLength = d[1];
} else {
messageLength = b.byteLength;
sizeLength = 0;
}
const end = messageLength + offset;
let cursor = offset + sizeLength;
while (cursor < end) {
const [key, keyLength] = decodeUint32(cursor, b);
const decoder = this.tagToDecoder[key];
const [data, numberOfBytes] = decoder(cursor + keyLength, b);
finalObject[this.tagToKey[key]] = data;
cursor += numberOfBytes + keyLength;
}
return [finalObject as AsObject<S>, messageLength + sizeLength];
}
decode(b: Readonly<Uint8Array>): AsObject<S> {
return this.decodeDelimited(0, b)[0];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment