Skip to content

Commit

Permalink
feat: implement timestamp (date) extension
Browse files Browse the repository at this point in the history
The only built-in extension described in the MessagePack spec. I think it's overly complex (both in the spec and my implementation), but it is what it is...
  • Loading branch information
norskeld committed Jul 29, 2023
1 parent 53fe74e commit 9feda71
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 22 deletions.
58 changes: 51 additions & 7 deletions src/deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export class Deserializer {

constructor(reader: DataReader, { extensionCodec }: DeserializerOptions = {}) {
this.reader = reader
this.textCodec = new TextDecoder()
this.extensionCodec = extensionCodec
this.textCodec = new TextDecoder()
this.refs = new Map()
}

Expand Down Expand Up @@ -150,25 +150,69 @@ export class Deserializer {
return map
}

private readRef(bytes: number): string {
private readRef(size: number): string {
// prettier-ignore
switch (bytes) {
switch (size) {
case 1: return this.refs.get(this.reader.readU8())!
case 2: return this.refs.get(this.reader.readU16())!

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

private readExt(bytes: number): unknown {
private readTimestamp(size: number): Date {
const data = this.reader.readRange(size)

switch (size) {
case 4: {
const sec =
((data[0] << 24) >>> 0) + ((data[1] << 16) >>> 0) + ((data[2] << 8) >>> 0) + data[3]

return new Date(sec * 1000)
}

case 8: {
const ns =
((data[0] << 22) >>> 0) +
((data[1] << 14) >>> 0) +
((data[2] << 6) >>> 0) +
(data[3] >>> 2)

const sec =
(data[3] & 0x3) * 4294967296 +
((data[4] << 24) >>> 0) +
((data[5] << 16) >>> 0) +
((data[6] << 8) >>> 0) +
data[7]

return new Date(sec * 1000 + ns / 1000000)
}

case 12: {
const ns =
((data[0] << 24) >>> 0) + ((data[1] << 16) >>> 0) + ((data[2] << 8) >>> 0) + data[3]

return new Date(data[4] * 1000 + ns / 1000000)
}

default: {
throw new DeserializationError(
`Invalid date size at ${this.reader.offset}. Expected 4, 8 or 12 bytes, but got ${size}.`
)
}
}
}

private readExt(size: number): unknown {
const extType = this.reader.readI8()

// prettier-ignore
switch (extType) {
case Extension.Ref: return this.readRef(bytes)
default: return this.extensionCodec?.decode(extType, this.reader.readRange(bytes))
case Extension.Timestamp: return this.readTimestamp(size)
case Extension.Ref: return this.readRef(size)
default: return this.extensionCodec?.decode(extType, this.reader.readRange(size))
}
}
}
13 changes: 12 additions & 1 deletion src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ describe('serialize/deserialize', () => {
it('string', () => {
backpack('hello')
backpack('привет')
backpack('こんにちは世界!')
})

it('number', () => {
backpack(0)
backpack(42)
backpack(4.2)
backpack(-300)

backpack(4.2)
backpack(0.000042)

backpack(0xff)
backpack(0b101010)
})

it('boolean', () => {
Expand All @@ -35,11 +41,16 @@ describe('serialize/deserialize', () => {
backpack([])
backpack([42, 69])
backpack(['hello', 'мир'])
backpack([{ message: 'hello world' }, { message: 'bonjour sac à dos' }])
})

it('object', () => {
backpack({ hello: 'world' })
backpack({ message: ['hello', 'world'] })
backpack({ compact: true, schema: 0 })
})

it('date', () => {
backpack(new Date(2020, 2, 22))
})
})
2 changes: 1 addition & 1 deletion src/rw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export class DataWriter {
return this
}

writeBytes(bytes: Uint8Array): this {
writeBytes(bytes: Uint8Array | number[]): this {
const length = bytes.length

if (length === 0) {
Expand Down
60 changes: 47 additions & 13 deletions src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export class Serializer {

constructor(writer: DataWriter, { extensionCodec }: SerializerOptions = {}) {
this.writer = writer
this.textCodec = new TextEncoder()
this.extensionCodec = extensionCodec
this.textCodec = new TextEncoder()
this.refs = new Map()
}

Expand All @@ -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 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 @@ -148,6 +149,39 @@ export class Serializer {
else this.writer.writeU8(Format.FixExt2).writeI8(Extension.Ref).writeU16(ref)
}

private writeTimestamp(date: Date): void {
const seconds = date.getTime() / 1000
const ms = date.getMilliseconds()

// 32-bit seconds
if (ms === 0 && seconds >= 0 && seconds <= 4294967295) {
this.writer
.writeU8(Format.FixExt4)
.writeI8(Extension.Timestamp)
.writeBytes([seconds >>> 24, seconds >>> 16, seconds >>> 8, seconds])
}
// 30-bit nanoseconds + 34-bit seconds
else if (seconds >= 0 && seconds <= 17179869183) {
const ns = ms * 1000000

this.writer
.writeU8(Format.FixExt8)
.writeI8(Extension.Timestamp)
.writeBytes([ns >>> 22, ns >>> 14, ns >>> 6, ((ns << 2) >>> 0) | (seconds / 4294967296)])
.writeBytes([seconds >>> 24, seconds >>> 16, seconds >>> 8, seconds])
}
// 32-bit nanoseconds + 64-bit seconds
else {
const ns = ms * 1000000

this.writer
.writeU8(Format.Ext8)
.writeI8(Extension.Timestamp)
.writeBytes([ns >>> 24, ns >>> 16, ns >>> 8, ns])
.writeI64(seconds)
}
}

private writeBinary(data: Uint8Array): void {
const length = data.length

Expand Down Expand Up @@ -211,26 +245,26 @@ export class Serializer {
}

const encoded = this.extensionCodec.encode(object)
const length = encoded.length
const size = encoded.length

// Resolving and writing extension format.

// Format: fixext 1
if (length == 1) this.writer.writeU8(Format.FixExt1)
if (size == 1) this.writer.writeU8(Format.FixExt1)
// Format: fixext 2
else if (length == 2) this.writer.writeU8(Format.FixExt2)
else if (size == 2) this.writer.writeU8(Format.FixExt2)
// Format: fixext 4
else if (length == 4) this.writer.writeU8(Format.FixExt4)
else if (size == 4) this.writer.writeU8(Format.FixExt4)
// Format: fixext 8
else if (length == 8) this.writer.writeU8(Format.FixExt8)
else if (size == 8) this.writer.writeU8(Format.FixExt8)
// Format: fixext 16
else if (length == 16) this.writer.writeU8(Format.FixExt16)
// Format: ext 8 + length (u8)
else if (length <= 255) this.writer.writeU8(Format.Ext8).writeU8(length)
// Format: ext 16 + length (u16)
else if (length <= 65535) this.writer.writeU8(Format.Ext16).writeU16(length)
// Format: ext 32 + length (u32)
else if (length <= 4294967295) this.writer.writeU8(Format.Ext32).writeU32(length)
else if (size == 16) this.writer.writeU8(Format.FixExt16)
// Format: ext 8 + size (u8)
else if (size <= 255) this.writer.writeU8(Format.Ext8).writeU8(size)
// Format: ext 16 + size (u16)
else if (size <= 65535) this.writer.writeU8(Format.Ext16).writeU16(size)
// Format: ext 32 + size (u32)
else if (size <= 4294967295) this.writer.writeU8(Format.Ext32).writeU32(size)
// Otherwise fail.
else throw new SerializationError(`Extension data is too big. Max (2^32)-1 bytes.`)

Expand Down

0 comments on commit 9feda71

Please sign in to comment.