Skip to content

Release v3.0.0

Pre-release
Pre-release
Compare
Choose a tag to compare
@OmniacDev OmniacDev released this 14 Jan 19:58
· 3 commits to main since this release

Moving from JSON to Binary

With v3.0.0, much of the library has been restructured to use binary serialization, rather than the previous JSON strings.

This brings a few performance improvements for complex value types, such as nested objects, and it also has a reduced footprint in the scriptevent messages, allowing for more data to be sent in a set amount of time, since less space is wasted with unnecessary data.

I personally find this version way nicer than previous versions, but I would like to hear everyone's opinions, and any suggestions I could make to further improve upon this.

This version will of course NOT be compatible with previous versions.

Serializable

Lets begin by exploring the "Serializable" types. These are types that can be serialized/deserialized, to and from their binary forms. v3.0.0 comes with a few of these already implemented (These are exported by the Proto class);

  • Int8
  • Int16
  • Int32
  • UInt8
  • UInt16
  • UInt32
  • Float32
  • Float64
  • VarInt
  • String
  • Boolean
  • UInt8Array
  • Date
  • Object
  • Array
  • Tuple
  • Optional
  • Map
  • Set

You can use these serializable types to create serializers for almost any type of value. Most of the types are simple static Serializable instances, however, a few of them are methods, used to create a new Serializable that reflects the input type structure.

For example, you can use the Proto.Object method, to create a serializer for an arbitrary object type, such as the ConnectionSerializer used by the Connection & ConnectionManager classes for communication;

const ConnectionSerializer = Proto.Object({
  from: Proto.String,
  bytes: Proto.UInt8Array
})

Which is then used internally with NET.emit;

yield* NET.emit(`ipc:${$._to}:${channel}:send`, ConnectionSerializer, {
  from: $._from, // $._from is a string
  bytes // bytes is a UInt8Array, maybe encrypted
})

Advanced

The Serializable system is pretty flexible, and allows users to define fully custom serializers. The Serializable interface is simple, having only two methods, serialize & deserialize. These are both generator functions;

export interface Serializable<T = any> {
  serialize(value: T, stream: ByteArray): Generator<void, void, void>
  deserialize(stream: ByteArray): Generator<void, T, void>
}

The ByteArray class is a simple dynamic wrapper over a UInt8Array, with a few extra methods, such as read and write.

read takes a parameter for the number of bytes to read, and then returns an array of bytes (numbers in range 0-255) with that many elements. If the number of elements requested is more than the remaining elements in the array, it only returns the remaining ones.

write takes any amount of bytes (numbers in range 0-255), and writes them onto the end of the buffer.

General

Now that we've explored the Serializables a bit, we can move on to the changes to the methods within the IPC & NET modules.

All the IPC methods now take a Serializable as an argument, with some taking more than one;

// serializer argument for the value being sent
function send<T>(channel: string, serializer: NET.Serializable<T>, value: T): void
// serializer argument for value being sent, and deserializer for expected return value.
function invoke<T, R>(channel: string, serializer: NET.Serializable<T>, value: T, deserializer: NET.Serializable<R>): Promise<R>;

These changes apply for all methods, including those in Connection and ConnectionManager classes.

Similarly, the NET.emit and NET.listen functions also take serializers as an argument.

Type Distribution

In previous versions, sharing types was essentially non-existant, due to the annoying way JS works. However, in this version, it has become INCREDIBLY easy. Since the system now relies on serializers which exist at runtime, we can create and export them from common files, that can be easily distributed to other users.

For example, this simple file can be placed in the same folder as the main IPC files, and can then be used in methods etc. This can then be shared as a file that the user just has to drop in, with no annoying modifications to the main IPC files needed to get it working.

import { Proto } from "./ipc";

export const TestSerializer = Proto.Object({
    value: Proto.String,
    tick: Proto.VarInt
})

What's Changed

Full Changelog: v2.0.0...v3.0.0