Skip to content

Commit

Permalink
Post-process parsed JSON to turn any necessary number as BigInt
Browse files Browse the repository at this point in the history
  The BigInt library has no way to know whether a type should be a
  normal number or a bigint when parsing, because there's no reflection
  available to JS code at runtime. Any type information from TypeScript
  is gone, and therefore, the library makes the choice of using bigint
  only when necessary.

  This is in practice annoying, as it causes TypeScript to behave as if
  some type was a BigInt, whereas they are in reality parsed as
  _number_, causing runtime errors upon the first manipulation.

  We have explored three possible solutions, and this commit implements
  the second one (while the third one is being worked but is more
  involved).

  1/ Return types as `number | bigint`, since this is what they are in
     the end. This pushes the concern down to the consumer which will
     have to deal with it in a quite ugly way.

  2/ Perform a second pass on the parsed data, to turn into bigint
  numbers which _should be_ (according to their type definition),
  bigint, but have been serialized as numbers. Not fully ideal because
  it requires to re-process the entire JSON object, and, it makes strong
  assumption on the shape of the JSON object to identify which fields
  should be turned into bigint and which one should be preserved. In the
  long run, this isn't very maintainable as it'll break as soon as we
  add a new field with a bigint type and forget to add it to the
  post-process checks.

  3/ Fork the bigint library, such that a JSON-schema can be given to
  the parser, which it can traverse while parsing a string, and consult
  to figure out _which_ type should a number be deserialized into. This
  is really non-trivial since JSON-schema supports compound operators
  such as `anyOf`, `oneOf` and `allOf`, requiring the parser to maintain
  a tree of possibilities which it attempts to narrow down as it
  traverse the tree.
  • Loading branch information
KtorZ committed Aug 11, 2021
1 parent dbe97ea commit 0a91b15
Showing 1 changed file with 54 additions and 1 deletion.
55 changes: 54 additions & 1 deletion clients/TypeScript/packages/client/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,60 @@ import { WebSocketClosed, TipIsOriginError } from './errors'
import JSONBig from 'json-bigint'

/** @internal */
export const safeJSON = JSONBig({ useNativeBigInt: true })
export const safeJSON = {
$: JSONBig({ useNativeBigInt: true }),

/* `sanitize` does a second pass after parsing, to convert into BigInt fields which should indeed be parsed
* as BigInt.
*
* Note that, this is potentially _slow_ since it needs to traverse the entire JSON.
*/
sanitize(json : any) : [any, boolean] {
if (typeof json === 'object' && json !== null) {
const len = Object.getOwnPropertyNames(json).length

// AssetQuantity
if (len === 2 && json.coins !== undefined && json.assets !== undefined) {
for (let k in json.assets) {
const v = json.assets[k];
json.assets[k] = typeof v === 'number' ? BigInt(v) : v
}
return [json, true]
}

// Metadatum@Int
if (len === 1 && json.int !== undefined) {
const v = json.int
json.int = typeof v === 'number' ? BigInt(v) : v
return [json, true]
}

// Otherwise...
let anyChanged = false
for (let k in json) {
const [v, changed] = this.sanitize(json[k])
// Only re-write the object if it _has_ changed. To keep things relatively fast.
if (changed) {
json[k] = v
anyChanged = true
}
}

return [json, anyChanged]
}

return [json, false]
},

parse(raw : string) : any {
return this.sanitize(this.$.parse(raw))[0]
},

stringify(...args : any[]) : string {
return this.$.stringify(...args)
}
}


/** @internal */
export const createPointFromCurrentTip = async (context?: InteractionContext): Promise<Point> => {
Expand Down

0 comments on commit 0a91b15

Please sign in to comment.