Skip to content
Snippets Groups Projects
index.ts 5.02 KiB
Newer Older
import { extractCommand, makeBuffer, search, searchParent } from "./util";
import {
  CommandType,
  CallFn,
  CommandObject,
  PbRequestMessage,
  RecipeObject,
  OnFn,
  DecodeResponse,
  DecodeOptions,
  USPVersion,
} from "../types";

import resolve from "./recipes/resolve";
import operateRecipe from "./recipes/operate";
import subscribe from "./recipes/subscribe";

import v10 from "./1.0,1.1";
import v12 from "./1.2";

const commands: Record<USPVersion, Record<CommandType, CommandObject>> = {
  "1.0": v10,
  "1.1": v10,
  "1.2": v12,
const recipes: RecipeObject[] = [resolve as any, operateRecipe, subscribe];

export const makeRecipes = (call: CallFn, on: OnFn): any =>
  recipes.reduce(
    (acc, { make, name }) => ({ ...acc, [name]: make(call, on) }),
    {}
  );

export const decodeId = (data: any) => String(data);
const unkownErr = (
  msg: Record<string, string>
): [string, string, Record<string, any>] => ["error", "", msg];

export const readMsg = (proto: Proto, data: any): Record<string, any> => {
  const record = proto.rootRecord.lookupType("usp_record.Record");
  const decodedRecord: any = record.decode(
    "binaryData" in data ? data.binaryData : data
  );
  const msg = proto.rootMsg.lookupType("usp.Msg");
  const decodedMsg = msg.decode(decodedRecord.noSessionContext.payload);

  return JSON.parse(JSON.stringify(decodedMsg)); // forces conversions
type DecodeFn = (parsedMsg: any, version: USPVersion) => DecodeResponse;

export const decode: DecodeFn = (parsedMsg, version: USPVersion) => {
  const err = searchParent(parsedMsg, "errMsg") || null;
  const command = extractCommand(parsedMsg);
  const foundId = search(parsedMsg, "msgId");
  // if id is formatted by me (command@) then use, otherwise check for sub id
  const id = foundId.includes("@")
    ? foundId
    : search(parsedMsg, "subscriptionId") || null;

  // if command is unkown
  if (!command) return unkownErr(parsedMsg);
  if (err) return [id, null, err, command];
  const cmd: CommandObject | null = commands[version][command] || null;
  if (!cmd) return unkownErr(parsedMsg);

  const [decodedData, decodedId, decodedErr] = cmd.decode(parsedMsg);
  return [decodedId || id, decodedData, decodedErr || err, command];
};

type DecodeWithOptionsFn = (
  parsedMsg: any,
  cmdType: CommandType | undefined,
  options: DecodeOptions,
  version: USPVersion
) => any;

export const decodeWithOptions: DecodeWithOptionsFn = (
  parsedMsg,
  cmdType,
  options,
  version: USPVersion
) => {
  const cmd: CommandObject | null =
    commands[version][cmdType as CommandType] || null;
  if (!cmd) return unkownErr(parsedMsg);

  const [decodedData] = cmd.decode(parsedMsg, options);
  return decodedData;
  (proto: Proto, options?: Record<string, string>) =>
    version: USPVersion,
    args: Record<string, any>
  ): [string, any, string | null] => {
    const cmd = commands[version][command] || null;
    if (!cmd) return ["error", null, `Uknown command: ${command}`];
    return [...convert(proto, cmd.encode(args), options)];
  proto: Proto,
  msg: PbRequestMessage,
  bufferOptions?: Record<string, string>
): [string, any, string | null] => {
  const id = msg.header.msgId;
  msg.header.msgType = proto.header.MsgType[msg.header.msgType];
  const converted = _convert(proto, msg);
  if (isError(converted)) return [id, null, converted];

  const encoded = proto.rootMsg
    .lookupType("usp.Msg")
    .encode(converted)
    .finish();
  const buffer = makeBuffer(proto.rootRecord, encoded, bufferOptions || {});
  return [id, buffer, null];
};

const isError = (o: any): o is string => typeof o == "string";

const internalKeys = ["lookup"];
const isInternal = (key: string) => internalKeys.includes(key);
const makePayload = (items: [string, any][], isArr: boolean) =>
  items
    .filter(([k]) => !isInternal(k))
    .reduce(
      (acc: any, [k, v]) =>
        isArr
          ? [...acc, v]
          : {
              ...acc,
              [k]: v,
            },
      isArr ? [] : {}
    );

const isStringArray = (obj: any) =>
  Array.isArray(obj) && obj.every((v) => typeof v === "string");
const needsConversion = (v: any) => typeof v === "object" && !isStringArray(v);

const _convert = (proto: Proto, value: any): string | any => {
  const skip = value.lookup === undefined;
  const lookup = "usp." + value.lookup;
  const item = skip ? null : proto.rootMsg.lookupType(lookup);

  const simpleValues: any[] = Object.entries(value).filter(
    ([, v]) => !needsConversion(v)
  );
  const toConvert = Object.entries(value).filter(([, v]) => needsConversion(v));
  const converted: [string, any][] = toConvert.map(([k, v]) => [
    k,
    _convert(proto, v),
  ]);

  const err = converted.find(([, v]) => isError(v));
  if (err) return err[1];

  const total = converted.concat(simpleValues);
  const payload = makePayload(total, Array.isArray(value));
  const payloadErr = item?.verify(payload);
  if (payloadErr) return payloadErr;

  return item ? item.create(payload) : payload;
};