-
-
Save jjbubudi/d293fe437e716151dea9753c9b4d78bb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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