@selfage/message
Version:
Runtime lib for generated messages to parse, copy, serialize and deserialize messages.
126 lines (94 loc) • 5.33 kB
Markdown
# /message
Utilities that keep plain JavaScript/TypeScript data in sync with a shared schema. Define the shape of a "message" once and use the helpers in this package to validate, clone, compare, stringify, and turn that data into a compact binary buffer.
## Why use it
- Schema-aware tools without a heavy runtime: describe fields once and reuse the same descriptor everywhere.
- Deterministic binary format for moving structured data between workers, processes, or services.
- Safer handling of untrusted input by filtering to known fields and types.
- Convenient helpers for deep copy, equality checks, and stable JSON output, all driven by the same descriptor.
- Optional CLI to serialize/deserialize from the command line for debugging or quick integrations.
## Install
```bash
npm install /message
```
## Core idea
A *message descriptor* is a plain object that lists every field in a message, its numeric index, and how that field should be treated (number, boolean, string, enum, nested message, or array). Descriptors can be generated or hand-written. Once you have one, every API in this package accepts that descriptor to understand the shape of your data.
```ts
import {
PrimitiveType,
MessageDescriptor,
serializeMessage,
deserializeMessage,
parseMessage,
} from "@selfage/message";
interface User {
id?: number;
nickname?: string;
active?: boolean;
}
const USER: MessageDescriptor<User> = {
name: "User",
fields: [
{ name: "id", index: 1, primitiveType: PrimitiveType.NUMBER },
{ name: "nickname", index: 2, primitiveType: PrimitiveType.STRING },
{ name: "active", index: 3, primitiveType: PrimitiveType.BOOLEAN },
],
};
const illFormedUser: any = {
id: "12",
nickname: "Ada",
active: "yes",
extraField: "ignored",
};
const parsed = parseMessage(illFormedUser, USER);
// parsed becomes { id: undefined, nickname: "Ada", active: undefined }
// If sent over the wire:
const user: User = { id: 12, nickname: "Ada", active: true };
const binary = serializeMessage(user, USER);
const roundTrip = deserializeMessage(binary, USER);
// roundTrip is now { id: 12, nickname: "Ada", active: true }
```
## Generating descriptors
Rather than hand-writing descriptor files, you can also generate them with `/generator_cli`. The tool reads a schema file and emits the TypeScript interface alongside the matching descriptor object.
```bash
npm install --save-dev /generator_cli
# definition.yaml must sit in the working directory; omit the .yaml suffix if you like
npx geneage ./definition.yaml
```
And the `definition.yaml` looks like:
```yaml
- kind: Message
name: User
fields:
- { name: id, type: number, index: 1 }
- { name: nickname, type: string, index: 2 }
- { name: active, type: boolean, index: 3 }
```
The generated module exports the same pair as the example above (a `User` interface and `USER` descriptor), ready to import into your project.
## Everyday helpers
- `parseMessage(raw, descriptor)` filters an arbitrary object, keeping only fields that match the descriptor and coercing enums/primitive types.
- `copyMessage(from, descriptor, to?)` deep-clones a message (optionally into an existing object) while reusing typed arrays and nested objects safely.
- `equalMessage(left, right, descriptor)` performs structural equality on messages, including nested objects and arrays.
- `stringifyMessage(message, descriptor)` produces an index-based JSON string that is stable and language-neutral; `destringifyMessage(raw, descriptor)` reverses the process.
- `serializeMessage(message, descriptor)` and `deserializeMessage(binary, descriptor)` convert between objects and the compact binary wire format shown in `serializer.ts`.
Helpers ignore unknown fields, drop singular fields that are `null`/`undefined`, and keep array positions even when the element value becomes `undefined`.
## Command line usage
Install locally (as shown above) or run via `npx`:
```bash
# Serialize a JSON object using a descriptor exported from ./descriptor.js
npx message serialize '{"id":1,"nickname":"Ada"}' \
--encoding hex \
--descriptor-file ./descriptor \
--descriptor-name USER
# Deserialize a base64 string back into JSON
npx message deserialize "AQAAAA..." \
--encoding base64 \
--descriptor-file ./descriptor \
--descriptor-name USER
```
The CLI dynamically imports the descriptor file, so it works with either `.js` or `.ts` sources (set `--descriptor-file` without the extension).
## Behavior notes
- Default buffer size is 16 MB; call `initBuffer(newSize)` before serializing if you need to handle larger messages.
- Field and enum indexes must fit inside a 32-bit unsigned integer. String lengths share the same limit (max `2^32 - 1` bytes).
- The library expects descriptors to list fields in ascending `index` order. This ensures fast lookups during deserialization.
- Singular fields skip serialization when the value is `undefined` *or* `null`, so deserialization produces `undefined`. Within arrays the element slot is preserved, but `null` or `undefined` entries come back as `undefined`.
With these building blocks you can keep your data definitions in one place and rely on a single set of utilities—from validation to binary transport—to keep messages consistent across your project.