Skip to content

Commit

Permalink
Implements a JSON.parse that transforms unsafe numbers to bigints
Browse files Browse the repository at this point in the history
  • Loading branch information
lorisleiva committed Aug 22, 2024
1 parent 04942c9 commit 86ed7d4
Show file tree
Hide file tree
Showing 12 changed files with 545 additions and 11 deletions.
2 changes: 1 addition & 1 deletion packages/rpc-api/src/__tests__/get-account-info-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('getAccountInfo', () => {
executable: false,
lamports: 5000000n,
owner: '11111111111111111111111111111111',
rentEpoch: 18446744073709551616n, // TODO: This number loses precision
rentEpoch: 18446744073709551615n,
space: 9n,
},
});
Expand Down
6 changes: 3 additions & 3 deletions packages/rpc-api/src/__tests__/get-multiple-accounts-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ describe('getMultipleAccounts', () => {
executable: false,
lamports: 5000000n,
owner: '11111111111111111111111111111111',
rentEpoch: 18446744073709551616n, // TODO: This number loses precision
rentEpoch: 18446744073709551615n,
space: 9n,
},
{
data: ['', 'base64'],
executable: false,
lamports: 5000000n,
owner: '11111111111111111111111111111111',
rentEpoch: 18446744073709551616n, // TODO: This number loses precision
rentEpoch: 18446744073709551615n,
space: 0n,
},
],
Expand Down Expand Up @@ -114,7 +114,7 @@ describe('getMultipleAccounts', () => {
executable: false,
lamports: 5000000n,
owner: '11111111111111111111111111111111',
rentEpoch: 18446744073709551616n,
rentEpoch: 18446744073709551615n,
space: 9n,
},
null,
Expand Down
6 changes: 3 additions & 3 deletions packages/rpc-api/src/__tests__/get-program-accounts-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('getProgramAccounts', () => {
executable: false,
lamports: 5000000n,
owner: 'DXngmJfjurhnAwbMPgpUGPH6qNvetCKRJ6PiD4ag4PTj',
rentEpoch: 18446744073709551616n, // TODO: This number loses precision
rentEpoch: 18446744073709551615n,
space: 9n,
},
pubkey: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
Expand Down Expand Up @@ -126,7 +126,7 @@ describe('getProgramAccounts', () => {
executable: false,
lamports: 5000000n,
owner: 'AmtpVzo6H6qQCP9dH9wfu5hfa8kKaAFpTJ4aamPYR6V6',
rentEpoch: 18446744073709551616n, // TODO: This number loses precision
rentEpoch: 18446744073709551615n,
space: 9n,
},
pubkey: 'C5q1p5UiCVrt6vcLJDGcS4AZ98fahKyb9XkDRdqATK17',
Expand All @@ -137,7 +137,7 @@ describe('getProgramAccounts', () => {
executable: false,
lamports: 5000000n,
owner: 'AmtpVzo6H6qQCP9dH9wfu5hfa8kKaAFpTJ4aamPYR6V6',
rentEpoch: 18446744073709551616n, // TODO: This number loses precision
rentEpoch: 18446744073709551615n,
space: 9n,
},
pubkey: 'Hhsoev7Apk5dMbktzLUrsTHuMq9e9GSYBaLcnN2PfdKS',
Expand Down
1 change: 1 addition & 0 deletions packages/rpc-transport-http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
],
"scripts": {
"benchmark": "./src/__benchmarks__/run.ts",
"benchmark:json-parse": "./src/__benchmarks__/json-parse.ts",
"compile:js": "tsup --config build-scripts/tsup.config.package.ts",
"compile:typedefs": "tsc -p ./tsconfig.declarations.json",
"dev": "jest -c ../../node_modules/@solana/test-config/jest-dev.config.ts --rootDir . --watch",
Expand Down
50 changes: 50 additions & 0 deletions packages/rpc-transport-http/src/__benchmarks__/json-parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env -S pnpm dlx tsx --

import { Bench } from 'tinybench';
import { fs, path } from 'zx';

import {
jsonParseWithLargeIntegersAsBigInts,
wrapLargeIntegersUsingParser,
wrapLargeIntegersUsingParserAndRegex,
wrapLargeIntegersUsingRegex,
} from '../json-parse-with-bigint';

Object.assign(globalThis, {
__BROWSER__: false,
__DEV__: false,
__NODEJS__: true,
__REACTNATIVE____: false,
});

const bench = new Bench({
throws: true,
});

const largeJsonPath = path.join(__dirname, '..', '__tests__', 'large-json-file.json');
const largeJsonString = fs.readFileSync(largeJsonPath, 'utf8');

bench
.add('JSON.parse', () => {
return JSON.parse(largeJsonString);
})
.add('JSON.parse with noop reviver', () => {
return jsonParseWithLargeIntegersAsBigInts(largeJsonString, x => x);
})
.add('jsonParseWithLargeIntegersAsBigInts (parser)', () => {
return jsonParseWithLargeIntegersAsBigInts(largeJsonString);
})
.add('jsonParseWithLargeIntegersAsBigInts (parser and regex)', () => {
return jsonParseWithLargeIntegersAsBigInts(largeJsonString, wrapLargeIntegersUsingParserAndRegex);
})
.add('jsonParseWithLargeIntegersAsBigInts (parser and noop)', () => {
return jsonParseWithLargeIntegersAsBigInts(largeJsonString, x => wrapLargeIntegersUsingParser(x, () => null));
})
.add('jsonParseWithLargeIntegersAsBigInts (regex)', () => {
return jsonParseWithLargeIntegersAsBigInts(largeJsonString, wrapLargeIntegersUsingRegex);
});

(async () => {
await bench.run();
console.table(bench.table());
})();
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ describe('createHttpTransport and `AbortSignal`', () => {
it('resolves with the response', async () => {
expect.assertions(1);
jest.mocked(fetchSpy).mockResolvedValueOnce({
json: () => ({ ok: true }),
ok: true,
text: () => '{"ok":true}',
} as unknown as Response);
const sendPromise = makeHttpRequest({ payload: 123, signal: abortSignal });
abortController.abort('I got bored waiting');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ describe('createHttpTransport', () => {
describe('when the endpoint returns a well-formed JSON response', () => {
beforeEach(() => {
fetchSpy.mockResolvedValue({
json: () => ({ ok: true }),
ok: true,
text: () => '{"ok":true}',
});
});
it('calls fetch with the specified URL', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import fs from 'fs';
import path from 'path';

import { jsonParseWithLargeIntegersAsBigInts, wrapLargeIntegers } from '../json-parse-with-bigint';

const MAX_SAFE_INTEGER: number = Number.MAX_SAFE_INTEGER;
const MAX_SAFE_INTEGER_PLUS_ONE: bigint = BigInt(Number.MAX_SAFE_INTEGER) + 1n;

describe('jsonParseWithLargeNumbersAsBigInts', () => {
it('parses large integers as bigints', () => {
const result = jsonParseWithLargeIntegersAsBigInts(`{"value":${MAX_SAFE_INTEGER_PLUS_ONE}}`) as {
value: unknown;
};
expect(result).toEqual({ value: MAX_SAFE_INTEGER_PLUS_ONE });
expect(typeof result.value).toBe('bigint');
});
it('keeps safe integers as numbers', () => {
const result = jsonParseWithLargeIntegersAsBigInts(`{"value":${MAX_SAFE_INTEGER}}`) as { value: unknown };
expect(result).toEqual({ value: MAX_SAFE_INTEGER });
expect(typeof result.value).toBe('number');
});
it('does not affect numbers in strings', () => {
expect(jsonParseWithLargeIntegersAsBigInts(`{"value":"Hello 123 World"}`)).toEqual({
value: 'Hello 123 World',
});
});
it('does not affect numbers in strings with escaped double quotes', () => {
expect(jsonParseWithLargeIntegersAsBigInts(`{"value":"Hello\\" 123 World"}`)).toEqual({
value: 'Hello" 123 World',
});
});
it('parses large integers with exponents as bigints', () => {
expect(jsonParseWithLargeIntegersAsBigInts(`1e32`)).toBe(100000000000000000000000000000000n);
expect(jsonParseWithLargeIntegersAsBigInts(`-1189e+32`)).toBe(-118900000000000000000000000000000000n);
});
it('does not affect negative exponents', () => {
expect(jsonParseWithLargeIntegersAsBigInts(`1e-32`)).toBe(1e-32);
expect(jsonParseWithLargeIntegersAsBigInts(`-1189e-32`)).toBe(-1189e-32);
});
it('can parse complex JSON files', () => {
const largeJsonPath = path.join(__dirname, 'large-json-file.json');
const largeJsonString = fs.readFileSync(largeJsonPath, 'utf8');
const expectedResult = JSON.parse(largeJsonString, (key, value) =>
// eslint-disable-next-line jest/no-conditional-in-test
key === 'lamports' ? 142302234983644260n : value,
);
expect(jsonParseWithLargeIntegersAsBigInts(largeJsonString)).toEqual(expectedResult);
});
});

describe('wrapLargeIntegers', () => {
it('wraps large integers as bigint strings', () => {
expect(wrapLargeIntegers(`{"value":${MAX_SAFE_INTEGER_PLUS_ONE}}`)).toBe(
`{"value":{"$n":"${MAX_SAFE_INTEGER_PLUS_ONE}"}}`,
);
});
it('keeps safe integers as numbers', () => {
expect(wrapLargeIntegers(`{"value":${MAX_SAFE_INTEGER}}`)).toBe(`{"value":${MAX_SAFE_INTEGER}}`);
});
it('does not wrap numbers in strings', () => {
expect(wrapLargeIntegers(`{"value":"Hello 123 World"}`)).toBe(`{"value":"Hello 123 World"}`);
});
it('does not wrap numbers in strings with escaped double quotes', () => {
expect(wrapLargeIntegers(`{"value":"Hello\\" 123 World"}`)).toBe(`{"value":"Hello\\" 123 World"}`);
});
it('does not alter special keywords', () => {
expect(wrapLargeIntegers('true')).toBe('true');
expect(wrapLargeIntegers('false')).toBe('false');
expect(wrapLargeIntegers('null')).toBe('null');
});
it('does not alter safe numbers', () => {
expect(wrapLargeIntegers('0')).toBe('0');
expect(wrapLargeIntegers('123456')).toBe('123456');
expect(wrapLargeIntegers('-123456')).toBe('-123456');
expect(wrapLargeIntegers('3.14')).toBe('3.14');
expect(wrapLargeIntegers('-3.14')).toBe('-3.14');
expect(wrapLargeIntegers('1e5')).toBe('1e5');
expect(wrapLargeIntegers('-1e5')).toBe('-1e5');
expect(wrapLargeIntegers('1E5')).toBe('1E5');
expect(wrapLargeIntegers('-1E5')).toBe('-1E5');
expect(wrapLargeIntegers(`${MAX_SAFE_INTEGER}`)).toBe(`${MAX_SAFE_INTEGER}`);
expect(wrapLargeIntegers(`-${MAX_SAFE_INTEGER}`)).toBe(`-${MAX_SAFE_INTEGER}`);
});
it('wraps unsafe large integers', () => {
expect(wrapLargeIntegers('1e32')).toBe('{"$n":"1e32"}');
expect(wrapLargeIntegers('-1e32')).toBe('{"$n":"-1e32"}');
expect(wrapLargeIntegers('1E32')).toBe('{"$n":"1E32"}');
expect(wrapLargeIntegers('-1E32')).toBe('{"$n":"-1E32"}');
expect(wrapLargeIntegers(`${MAX_SAFE_INTEGER_PLUS_ONE}`)).toBe(`{"$n":"${MAX_SAFE_INTEGER_PLUS_ONE}"}`);
expect(wrapLargeIntegers(`-${MAX_SAFE_INTEGER_PLUS_ONE}`)).toBe(`{"$n":"-${MAX_SAFE_INTEGER_PLUS_ONE}"}`);
});
it('does not alter unsafe large floating points', () => {
expect(wrapLargeIntegers('3.14e32')).toBe('3.14e32');
expect(wrapLargeIntegers('-3.14e32')).toBe('-3.14e32');
expect(wrapLargeIntegers('3.14E32')).toBe('3.14E32');
expect(wrapLargeIntegers('-3.14E32')).toBe('-3.14E32');
expect(wrapLargeIntegers(`${MAX_SAFE_INTEGER_PLUS_ONE}.123`)).toBe(`${MAX_SAFE_INTEGER_PLUS_ONE}.123`);
expect(wrapLargeIntegers(`-${MAX_SAFE_INTEGER_PLUS_ONE}.123`)).toBe(`-${MAX_SAFE_INTEGER_PLUS_ONE}.123`);
});
it('does not alter strings', () => {
expect(wrapLargeIntegers('""')).toBe('""');
expect(wrapLargeIntegers('"Hello World"')).toBe('"Hello World"');
});
});
Loading

0 comments on commit 86ed7d4

Please sign in to comment.