camunda / camunda-8-js-sdk

The Camunda 8 JavaScript SDK for Node.js
https://camunda.github.io/camunda-8-js-sdk/
Apache License 2.0
19 stars 6 forks source link

Normalise API int64 type across Node SDK #78

Closed jwulf closed 8 months ago

jwulf commented 8 months ago

The JSON number value passed by the REST API for processInstanceKey (for example) is of type $int64.

This can't be reliably represented by the JavaScript number type, which is a 2^53 range floating point value.

This means that systems that are running for a long time can generate keys that cannot be represented by the SDK.

The gRPC library deals with this by parsing int64 to a string representation. Since we don't do arithmetic on keys, it might as well be a string.

The REST library that I am currently using, however, (got) represents these as number, leading to two issues:

  1. The imprecision issue that will become an L1 in production when a system hits key values that cannot be represented as the JS number type.

  2. The impedance mismatch that ZeebeGrpcClient returns long keys as type string, and the other APIs expect a number type as input; and vice versa.

I'm looking for alternative REST clients that can serialise long integer types as JavaScript string.

jwulf commented 8 months ago

OK, got supports custom JSON parse/stringify functions:

https://github.com/sindresorhus/got/blob/main/documentation/2-options.md#parsejson

This library could be used:

https://github.com/josdejong/lossless-json

jwulf commented 8 months ago

Hmmm, OK. I got it working. I can use a custom JSON parser with got to turn all numbers into LosslessNumbers.

ZeebeGrpc returns int64 types as type string, and leaves int32 numbers as number. This is consistent, and it is based on introspection of the protocol.

We can't detect the type of the field over REST. We know it is a number, but we don't know if it is an int32 or int64 field from the data itself.

I could encode this in the typing of the Dtos and use intellisense to determine whether the LosslessNumber should be marshalled to a string or to a number.

Going back the other way does not seem to be a problem.

Let me check if an API that takes an int64 number can accept a string. That could be a problem for the intellisense approach, because then we have two contracts to enforce - one with the API and the other with the application code.

jwulf commented 8 months ago

REST fields that take an int64 can take a string.

So, how can we emulate the behaviour of the ZeebeGrpcClient, and marshal int64 values to string and int32 values to number?

There must be an I/O boundary library that allows you to define the schema and that generates both the Dto typings and the runtime serialisers. I'll look at that tomorrow.

Maybe this? https://github.com/JohnWeisz/TypedJSON

Or this? https://github.com/colinhacks/zod

jwulf commented 8 months ago

OK, I have an approach for this.

A custom JSON parser that uses lossless-json to parse all JSON number types to a LosslessNumber.

The custom parser takes as an argument a Dto class. The class has decorated properties. The custom parser introspects the class and reads the type:Int32 and type:Int64 annotations, then uses those to convert the LosslessNumber to either a JS number or a JS string.

The class thus acts as both the metadata for the parser, via decorators on a Dto class, and also the type information for the consumer via the class interface.

jwulf commented 8 months ago

I've extended this to deal with nested Dtos.

It's in lib/LosslessJsonParser.ts

jwulf commented 8 months ago

This feature is now complete.

All known int64 type values are parsed to a JavaScript string type.

With unknown value types that come in via JSON number values, they are parsed to number unless they contain an unsafe int64 value, in which case the parser throws an exception.

A mapping Dto class can be provided that specifies mapping a JSON number field to either a string or bigint type. The parser will map the number value to this type, and the stringify function will do the reverse mapping.