Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework rpcmessage #37

Merged
merged 2 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/check-version-bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# vim: sw=2
name: Misc

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
check-version-bump:
name: Check version bump
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Check if version bumped
uses: del-systems/[email protected]

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "libshv-js",
"version": "3.3.6",
"version": "3.4.0",
"description": "Typescript implementation of libshv",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
Expand Down
2 changes: 1 addition & 1 deletion src/chainpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class ChainPackReader {
}

const implReturn = (x: RpcValueType) => {
const ret = meta !== undefined ? new RpcValueWithMetaData(x, meta) : x;
const ret = meta !== undefined ? new RpcValueWithMetaData(meta, x) : x;
return ret;
};

Expand Down
2 changes: 1 addition & 1 deletion src/cpon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class CponReader {
}

const implReturn = (x: RpcValueType) => {
const ret = meta !== undefined ? new RpcValueWithMetaData(x, meta) : x;
const ret = meta !== undefined ? new RpcValueWithMetaData(meta, x) : x;
return ret;
};

Expand Down
237 changes: 57 additions & 180 deletions src/rpcmessage.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import {type IMap, type Int, isIMap, makeIMap, makeMetaMap, type MetaMap, type RpcValue, RpcValueWithMetaData} from './rpcvalue';
import {toCpon} from './cpon';
import {toChainPack} from './chainpack';
import * as z from './zod';

enum RpcMessageTag {
RequestId = 8,
ShvPath = 9,
Method = 10,
CallerIds = 11,
}
export const RPC_MESSAGE_REQUEST_ID = 8;
export const RPC_MESSAGE_SHV_PATH = 9;
export const RPC_MESSAGE_METHOD = 10;
export const RPC_MESSAGE_CALLER_IDS = 11;

enum RpcMessageKey {
Params = 1,
Result = 2,
Error = 3,
}
export const RPC_MESSAGE_PARAMS = 1;
export const RPC_MESSAGE_RESULT = 2;
export const RPC_MESSAGE_ERROR = 3;

export const ERROR_CODE = 1;
export const ERROR_MESSAGE = 2;
Expand All @@ -34,169 +28,52 @@ enum ErrorCode {
NotImplemented = 12,
}

export type ErrorMap = {
[ERROR_CODE]: ErrorCode;
[ERROR_MESSAGE]?: string;
[ERROR_DATA]?: RpcValue;
};

class RpcError extends Error {
constructor(private readonly err_info: ErrorMap) {
super(err_info[ERROR_MESSAGE] ?? 'Unknown RpcError');
}

data() {
return this.err_info[ERROR_DATA];
}
}

export class ProtocolError extends Error {}

export class InvalidRequest extends RpcError {}
export class MethodNotFound extends RpcError {}
export class InvalidParams extends RpcError {}
export class InternalError extends RpcError {}
export class ParseError extends RpcError {}
export class MethodCallTimeout extends RpcError {}
export class MethodCallCancelled extends RpcError {}
export class MethodCallException extends RpcError {}
export class Unknown extends RpcError {}
export class LoginRequired extends RpcError {}
export class UserIDRequired extends RpcError {}
export class NotImplemented extends RpcError {}

class RpcMessage {
value: IMap;
meta: MetaMap;
constructor(rpc_val?: RpcValue) {
if (rpc_val === undefined) {
this.value = makeIMap({});
this.meta = makeMetaMap({});
return;
}

if (!(rpc_val instanceof RpcValueWithMetaData && isIMap(rpc_val.value))) {
throw new TypeError(`RpcMessage initialized with a non-IMap: ${toCpon(rpc_val)}`);
}

this.value = rpc_val.value;
this.meta = rpc_val.meta;
}

isValid() {
return this.shvPath() && (this.isRequest() || this.isResponse || this.isSignal());
}

isRequest(): boolean {
return this.requestId() !== undefined && this.method() !== undefined;
}

isResponse(): boolean {
return this.requestId() !== undefined && this.method() === undefined;
}

isSignal(): boolean {
return this.requestId() === undefined && this.method() !== undefined;
}

requestId(): Int | undefined {
return (this.meta[RpcMessageTag.RequestId] as Int);
}

setRequestId(id: number) {
this.meta[RpcMessageTag.RequestId] = id;
}

callerIds(): RpcValue[] | undefined {
return this.meta[RpcMessageTag.CallerIds] as RpcValue[];
}

setCallerIds(ids: RpcValue[]) {
this.meta[RpcMessageTag.CallerIds] = ids;
}

shvPath(): string | undefined {
return (this.meta[RpcMessageTag.ShvPath] as string);
}

setShvPath(val: string) {
this.meta[RpcMessageTag.ShvPath] = val;
}

method(): string | undefined {
return (this.meta[RpcMessageTag.Method] as string);
}

setMethod(val: string) {
this.meta[RpcMessageTag.Method] = val;
}

params() {
return this.value[RpcMessageKey.Params] as RpcValue;
}

setParams(params: RpcValue) {
this.value[RpcMessageKey.Params] = params;
}

resultOrError() {
if (Object.hasOwn(this.value, RpcMessageKey.Error)) {
if (!isIMap(this.value[RpcMessageKey.Error])) {
return new ProtocolError('Response had an error, but this error was not a map');
}

const errorMap = this.value[RpcMessageKey.Error];
if (typeof errorMap[ERROR_CODE] !== 'number') {
return new ProtocolError('Response had an error, but this error did not contain at least an error code');
}

const code = errorMap[ERROR_CODE] as unknown;

const ErrorTypeCtor = (() => {
switch (code) {
case ErrorCode.InvalidRequest: return InvalidRequest;
case ErrorCode.MethodNotFound: return MethodNotFound;
case ErrorCode.InvalidParams: return InvalidParams;
case ErrorCode.InternalError: return InternalError;
case ErrorCode.ParseError: return ParseError;
case ErrorCode.MethodCallTimeout: return MethodCallTimeout;
case ErrorCode.MethodCallCancelled: return MethodCallCancelled;
case ErrorCode.MethodCallException: return MethodCallException;
case ErrorCode.Unknown: return Unknown;
case ErrorCode.LoginRequired: return LoginRequired;
case ErrorCode.UserIDRequired: return UserIDRequired;
case ErrorCode.NotImplemented: return NotImplemented;
default: return Unknown;
}
})();

return new ErrorTypeCtor(this.value[RpcMessageKey.Error] as IMap<ErrorMap>);
}

if (Object.hasOwn(this.value, RpcMessageKey.Result)) {
return this.value[RpcMessageKey.Result] as RpcValue;
}

return new ProtocolError('Response included neither result nor error');
}

setResult(result: RpcValue) {
this.value[RpcMessageKey.Result] = result;
}

setError(error: string) {
this.value[RpcMessageKey.Error] = error;
}

toCpon() {
return toCpon(new RpcValueWithMetaData(this.value, this.meta));
}

toChainPack() {
return toChainPack(new RpcValueWithMetaData(this.value, this.meta));
}
}

export type RpcResponse<T = RpcValue> = T | Error;

export {RpcMessage, RpcError, ErrorCode};
const ErrorMapZod = z.imap({
[ERROR_CODE]: z.number(),
[ERROR_MESSAGE]: z.string().optional(),
[ERROR_DATA]: z.rpcvalue().optional(),
});

export type ErrorMap = z.infer<typeof ErrorMapZod>;

const RpcRequestMetaZod = z.metamap({
[RPC_MESSAGE_REQUEST_ID]: z.number(),
[RPC_MESSAGE_METHOD]: z.string(),
[RPC_MESSAGE_SHV_PATH]: z.string(),
});

const RpcRequestValueZod = z.imap({
[RPC_MESSAGE_PARAMS]: z.rpcvalue().optional(),
});

const RpcResponseMetaZod = z.metamap({
[RPC_MESSAGE_REQUEST_ID]: z.number(),
});
const RpcResponseValueZod = z.imap({
[RPC_MESSAGE_RESULT]: z.rpcvalue(),
}).or(z.imap({
[RPC_MESSAGE_ERROR]: ErrorMapZod,
}));

const RpcSignalMetaZod = z.metamap({
[RPC_MESSAGE_SHV_PATH]: z.string(),
[RPC_MESSAGE_METHOD]: z.string(),
});
const RpcSignalValueZod = z.imap({
[RPC_MESSAGE_PARAMS]: z.rpcvalue().optional(),
});

const RpcRequestZod = z.withMeta(RpcRequestMetaZod, RpcRequestValueZod);
const RpcResponseZod = z.withMeta(RpcResponseMetaZod, RpcResponseValueZod);
const RpcSignalZod = z.withMeta(RpcSignalMetaZod, RpcSignalValueZod);
const RpcMessageZod = z.union([RpcRequestZod, RpcResponseZod, RpcSignalZod]);
export type RpcRequest = z.infer<typeof RpcRequestZod>;
export type RpcResponse = z.infer<typeof RpcResponseZod>;
export type RpcSignal = z.infer<typeof RpcSignalZod>;
export type RpcMessage = z.infer<typeof RpcMessageZod>;

export const isSignal = (message: RpcMessage): message is RpcSignal => !(RPC_MESSAGE_REQUEST_ID in message.meta) && RPC_MESSAGE_METHOD in message.meta;
export const isRequest = (message: RpcMessage): message is RpcRequest => RPC_MESSAGE_REQUEST_ID in message.meta && RPC_MESSAGE_METHOD in message.meta;
export const isResponse = (message: RpcMessage): message is RpcResponse => RPC_MESSAGE_REQUEST_ID in message.meta && !(RPC_MESSAGE_METHOD in message.meta);

export {RpcMessageZod, ErrorCode};
8 changes: 5 additions & 3 deletions src/rpcvalue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ const isShvMap = (x: unknown): x is ShvMap => typeof x === 'object' && (x as Shv

const isIMap = (x: unknown): x is IMap => typeof x === 'object' && (x as IMap)[shvMapType] === 'imap';

const isMetaMap = (x: unknown): x is IMap => typeof x === 'object' && (x as MetaMap)[shvMapType] === 'metamap';

const makeMetaMap = <T extends Record<string | number, RpcValue> = Record<string | number, RpcValue>, U extends Record<number, RpcValue> = Omit<T, typeof shvMapType>>(x: U = {} as U): MetaMap<U> => ({
...x,
[shvMapType]: 'metamap',
Expand All @@ -112,10 +114,10 @@ const makeMap = <T extends Record<string, RpcValue> = Record<string, RpcValue>,
[shvMapType]: 'map',
});

class RpcValueWithMetaData {
constructor(public value: RpcValueType, public meta: MetaMap) {}
class RpcValueWithMetaData<MetaSchema extends MetaMap = MetaMap, ValueSchema extends RpcValueType = RpcValueType> {
constructor(public meta: MetaSchema, public value: ValueSchema) {}
}

export type RpcValue = RpcValueType | RpcValueWithMetaData;

export {shvMapType, Decimal, Double, type IMap, type MetaMap, RpcValueWithMetaData, type ShvMap, UInt, withOffset, makeMap, makeIMap, makeMetaMap, isIMap, isShvMap};
export {shvMapType, Decimal, Double, type IMap, type MetaMap, RpcValueWithMetaData, type ShvMap, UInt, withOffset, makeMap, makeIMap, makeMetaMap, isIMap, isMetaMap, isShvMap};
Loading
Loading