JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 49
  • Score
    100M100P100Q62286F
  • License GPL-3.0-or-later

Runtime lib for generated messages.

Package Exports

  • @selfage/message/copier
  • @selfage/message/copier.js
  • @selfage/message/descriptor
  • @selfage/message/descriptor.js
  • @selfage/message/merger
  • @selfage/message/merger.js
  • @selfage/message/parser
  • @selfage/message/parser.js
  • @selfage/message/test_matcher
  • @selfage/message/test_matcher.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@selfage/message) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

@selfage/message

Install

npm install @selfage/message

Overview

Written in TypeScript and compiled to ES6 with inline source map & source. See @selfage/tsconfig for full compiler options. Provides the runtime lib to be used together with message descriptors generated by @selfage/generator_cli, which can parse, copy and merge messages.

The term "message" stands for data class, inspired from Google's Protocol Buffers, i.e., in JavaScript/TypeScript case, an object without any functions defined on it, which is what can be communicated between different threads, processes, or distributed servers.

TypeScript uses interfaces to describe objects at compiling time, checking for invalid references to object fields/properties. However, in cases such as casting JSON.parse(...) to a type-safe object, JSON.parse(...) as MyData doesn't really validate fields for you and thus you don't get a real type-safe object. This runtime lib together with the generated message descriptor, can help validate and type-cast objects.

Generate MessageDescriptor

@selfage/generator_cli requires a JSON file as input, e.g. basic.json as the following.

[{
  "message": {
    "name": "BasicData",
    "fields": [{
      "name": "numberField",
      "type": "number"
    }, {
      "name": "stringArrayField",
      "type": "string",
      "isArray": true
    }]
  }
}]

It's just like a TypeScript interface but a little bit verbose when written in JSON. The schema of the JSON file is an array of definition.

By running $ geneage -d basic, basic.ts will be generated as the following.

import { MessageDescriptor, PrimitiveType } from '@selfage/message/descriptor';

export interface BasicData {
  numberField?: number,
  stringArrayField?: Array<string>
}

export let BASIC_DATA: MessageDescriptor<BasicData> = {
  name: 'BasicData',
  fields: [
    {
      name: 'numberField',
      primitiveType: PrimitiveType.NUMBER,
    },
    {
      name: 'stringArrayField',
      primitiveType: PrimitiveType.STRING,
      isArray: true
    },
  ]
};

It's recommended to commit basic.ts as a source file such that any code change on @selfage/generator_cli will not break your program.

Parse messages

The typical use case is to parse a JSON object, which actually validates the fields, rather than a simple type-cast.

import { parseMessage } from '@selfage/message/parser';
import { BASIC_DATA, BasicData } from './basic'; // As generated from the example above.

let raw = JSON.parse(`{ "numberField": 111, "otherField": "random", "stringArrayField": ["str1", "str2"] }`);
let basicData = parseMessage(raw, BASIC_DATA); // Of type `BasicData`.
basicData.numberField; // 111
basicData.stringArrayField; // ["str1", "str2"]
basicData.otherField; // undefined

You can also supply an in-place output object.

let output: BasicData = {};
parseMessage(raw, BASIC_DATA, output);

Note that it will overwrite everything in output.

Generate EnumDescriptor

TypeScript preserves enum information at runtime. Therefore, EnumDescriptor only exists for MessageDescriptor to reference.

An example JSON file, color.json, is as the following.

[{
  "enum": {
    "name": "Color",
    "values": [{
      "name": "RED",
      "value": 12
    }, {
      "name": "BLUE",
      "value": 1
    }]
  }
}]

By running $ geneage -d color, color.ts will be generated as the following.

import { EnumDescriptor } from '@selfage/message/descriptor';

export enum Color {
  RED = 12,
  BLUE = 1,
}

export let COLOR: EnumDescriptor<Color> = {
  name: 'Color',
  values: [
    {
      name: 'RED',
      value: 12,
    },
    {
      name: 'BLUE',
      value: 1,
    },
  ]
}

Copy messages

Technically, parseMessage can be used to copy messages as well. However, copyMessage performs slightly better by dropping field type checks.

import { copyMessage } from '@selfage/message/copier';
import { BASIC_DATA, BasicData } from './basic'; // As generated from the example above.

let basicData: BasicData = { numberField: 111 };
let dest = copyMessage(basicData, BASIC_DATA);
// Or in-place copy.
let dest2: BasicData = {};
copyMessage(data, BASIC_DATA, dest2);

Merge messages

If provided with a destination/existing message, both parseMessage and copyMessage will replace every field with the new one. mergeMessage, however, will only overwrite a field if the corresponding new field actually has a value.

import { mergeMessage } from '@selfage/message/merger';
import { BASIC_DATA, BasicData } from './basic'; // As generated from the example above.

let source: BasicData = { stringArrayField: ["123"] };
let existing: BasicData = { numberField: 111 };
mergeMessage(source, BASIC_DATA, existing);
// Now `existing` becomes: { numberField: 111, stringArrayField: ["123"] }

Test matcher

By importing @selfage/message/test_matcher, you can use it together with @selfage/test_matcher to match messages.

import { BasicData, BASIC_DATA } from './basic'; // As generated from the example above.
import { eqMessage } from '@selfage/message/test_matcher';
import { assertThat } from '@selfage/test_matcher'; // Install @selfage/test_matcher

let basicData: BasicData = { numberField: 111 };
assertThat(basicData, eqMessage({ numberField: 111 }, BASIC_DATA), `basicData`);

Design considerations for message

We didn't invent a new language/syntax as what Google's Protocol Buffers did, because:

  1. It involves a syntax parser.
  2. It's not scalable, if we want to support creative attributes of messages. You might end up inventing many weird syntax.

The downside with using JSON is obviously that it's verbose to type {} [] "" :, especially "name" a lot.

However, data size/compression has nothing to do with this approach. Because you can compress JSON strings easily with many popular tools.

We might introduce an field number to each field of message, when an easy-to-use compressor is identified.