Skip to content

Commit

Permalink
feat: implement map extension
Browse files Browse the repository at this point in the history
  • Loading branch information
norskeld committed Jul 29, 2023
1 parent 9feda71 commit 2682c70
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 33 deletions.
68 changes: 55 additions & 13 deletions src/deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,11 @@ export class Deserializer {
case Format.Ext32: return this.readExt(this.reader.readU32())

// Otherwise fail.
default: throw new DeserializationError(
`Unrecognized BackPack type 0x${type.toString(16)} at ${this.reader.offset}.`
)
default: {
throw new DeserializationError(
`Unrecognized BackPack type 0x${type.toString(16)} at ${this.reader.offset}.`
)
}
}
}

Expand Down Expand Up @@ -136,30 +138,68 @@ export class Deserializer {
}

private readObject(length: number): Record<string, unknown> {
const map: Record<string, unknown> = {}
const object: Record<string, unknown> = {}

while (length > 0) {
const key = this.decode() as string
const value = this.decode()

map[key] = value
object[key] = value

--length
}

return object
}

private readMap(length: number): Map<unknown, unknown> {
let size: number

// prettier-ignore
switch (length) {
case 1: size = this.reader.readU8(); break
case 2: size = this.reader.readU16(); break
case 4: size = this.reader.readU32(); break

default: {
throw new DeserializationError(
`Invalid map size marker at ${this.reader.offset}. ` +
`Expected 1, 2 or 4 bytes, but got ${length}.`
)
}
}

const map: Map<unknown, unknown> = new Map()

while (size > 0) {
const key = this.decode() as string
const value = this.decode()

map.set(key, value)

--size
}

return map
}

private readRef(size: number): string {
private readRef(length: number): string {
let size: number

// prettier-ignore
switch (size) {
case 1: return this.refs.get(this.reader.readU8())!
case 2: return this.refs.get(this.reader.readU16())!
switch (length) {
case 1: size = this.reader.readU8(); break
case 2: size = this.reader.readU16(); break

default: throw new DeserializationError(
`Invalid reference size at ${this.reader.offset}. Expected 1-2 bytes, but got ${size}.`
)
default: {
throw new DeserializationError(
`Invalid reference size marker at ${this.reader.offset}. ` +
`Expected 1 or 2 bytes, but got ${length}.`
)
}
}

return this.refs.get(size)!
}

private readTimestamp(size: number): Date {
Expand Down Expand Up @@ -199,7 +239,8 @@ export class Deserializer {

default: {
throw new DeserializationError(
`Invalid date size at ${this.reader.offset}. Expected 4, 8 or 12 bytes, but got ${size}.`
`Invalid date size marker at ${this.reader.offset}. ` +
`Expected 4, 8 or 12 bytes, but got ${size}.`
)
}
}
Expand All @@ -212,6 +253,7 @@ export class Deserializer {
switch (extType) {
case Extension.Timestamp: return this.readTimestamp(size)
case Extension.Ref: return this.readRef(size)
case Extension.Map: return this.readMap(size)
default: return this.extensionCodec?.decode(extType, this.reader.readRange(size))
}
}
Expand Down
40 changes: 28 additions & 12 deletions src/formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,25 +64,41 @@ export const enum Format {
/** Built-in BackPack extensions. */
export const enum Extension {
/**
* **Timestamp extension type**
* [Specification extension][spec-timestamp] for serializing/deserializing {@link Date} objects.
*
* Used to serialize and deserialize {@link Date} objects.
*
* This extension type is assigned to extension type `-1`. It defines 3 formats: 32-bit format,
* 64-bit format, and 96-bit format.
* [spec-timestamp]: https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type
*/
Timestamp = -1,

/**
* **Reference extension type**
*
* Opt-in extension used for "compressing" repeating data even further.
* Built-in extension for "compressing" repeating string data.
*
* Works by replacing short (4-16 bytes) repeating strings and property names with 1-2 bytes long
* ids (references), which will be replaced back with associated strings when deserializing.
* monotonically increasing ids (references), which will be replaced back with associated strings
* when deserializing.
*
* ### Formats
*
* - 8-bit
* - 16-bit
*
* Each represents references of (2^N)-1 at most, where N is the number of format's bits.
*/
Ref = -128,

/**
* Built-in extension for serializing/deserializing JavaScript {@link Map}. This is **not** the
* same map defined in the [MessagePack specification][spec-map].
*
* ### Formats
*
* - 8-bit
* - 16-bit
* - 32-bit
*
* Each represents maps with (2^N)-1 entries at most, where N is the number of format's bits.
*
* This extension type is assigned to extension type `-128` to avoid collisions. It defines 2
* formats: 8-bit format and 16-bit format.
* [spec-map]: https://github.com/msgpack/msgpack/blob/master/spec.md#map-format-family
*/
Ref = -128
Map = -127
}
38 changes: 38 additions & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,42 @@ describe('serialize/deserialize', () => {
it('date', () => {
backpack(new Date(2020, 2, 22))
})

it('map', () => {
backpack(
new Map([
['hello', 42],
['blazing', 420],
['turning', 180],
['warhammer', 9000]
])
)

backpack({
map: new Map([
[42, 'is the meaning'],
[420, 'is for blazing'],
[69, 'is for pleasure'],
[-1000, 'is for the account balance']
])
})

backpack([
new Map([
['hello', { message: 'hello' }],
['bye', { message: 'bye' }]
]),
new Map([
[{ kind: 1 }, 'the first kind'],
[{ kind: 2 }, 'the second kind']
])
])

backpack(
new Map([
[20200220, new Date(2020, 1, 20)],
[19901111, new Date(1990, 10, 11)]
])
)
})
})
38 changes: 30 additions & 8 deletions src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export class Serializer {
// Try to apply extensions first in case `data` extends `UIint8Array` or `Array`.
if (this.writeExt(data)) return

if (data instanceof Map) return this.writeMap(data)
if (data instanceof Date) return this.writeTimestamp(data)
if (data instanceof Uint8Array) return this.writeBinary(data)
if (Array.isArray(data)) return this.writeArray(data)
Expand Down Expand Up @@ -216,14 +217,14 @@ export class Serializer {

private writeObject(o: object): void {
const entries = Object.entries(o)
const length = entries.length

// Format: fixmap + length (up to 15 elements)
if (length <= 15) this.writer.writeU8(Format.FixMap | length)
// Format: map + length (u16)
else if (length <= 65535) this.writer.writeU8(Format.Map16).writeU16(length)
// Format: map + length (u32)
else if (length <= 4294967295) this.writer.writeU8(Format.Map32).writeU32(length)
const size = entries.length

// Format: fixmap + size (up to 15 elements)
if (size <= 15) this.writer.writeU8(Format.FixMap | size)
// Format: map + size (u16)
else if (size <= 65535) this.writer.writeU8(Format.Map16).writeU16(size)
// Format: map + size (u32)
else if (size <= 4294967295) this.writer.writeU8(Format.Map32).writeU32(size)
// Otherwise fail.
else throw new SerializationError('Object is too big. Max (2^32)-1 entries.')

Expand All @@ -233,6 +234,27 @@ export class Serializer {
}
}

private writeMap(m: Map<unknown, unknown>): void {
const entries = m.entries()
const size = m.size

// Format: fixext 1 + extension map + size (u8)
if (size <= 255) this.writer.writeU8(Format.FixExt1).writeI8(Extension.Map).writeU8(size)
// Format: fixext 2 + extension map + size (u16)
else if (size <= 65535)
this.writer.writeU8(Format.FixExt2).writeI8(Extension.Map).writeU16(size)
// Format: fixext 4 + extension map + size (u32)
else if (size <= 4294967295)
this.writer.writeU8(Format.FixExt4).writeI8(Extension.Map).writeU32(size)
// Otherwise fail.
else throw new SerializationError('Map is too big. Max (2^32)-1 entries.')

for (const [key, value] of entries) {
this.encode(key)
this.encode(value)
}
}

private writeExt(object: unknown): boolean {
if (!this.extensionCodec) {
return false
Expand Down

0 comments on commit 2682c70

Please sign in to comment.