You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

256 lines
8.5 KiB

1 month ago
  1. # json-ext
  2. [![NPM version](https://img.shields.io/npm/v/@discoveryjs/json-ext.svg)](https://www.npmjs.com/package/@discoveryjs/json-ext)
  3. [![Build Status](https://github.com/discoveryjs/json-ext/actions/workflows/ci.yml/badge.svg)](https://github.com/discoveryjs/json-ext/actions/workflows/ci.yml)
  4. [![Coverage Status](https://coveralls.io/repos/github/discoveryjs/json-ext/badge.svg?branch=master)](https://coveralls.io/github/discoveryjs/json-ext?)
  5. [![NPM Downloads](https://img.shields.io/npm/dm/@discoveryjs/json-ext.svg)](https://www.npmjs.com/package/@discoveryjs/json-ext)
  6. A set of utilities that extend the use of JSON. Designed to be fast and memory efficient
  7. Features:
  8. - [x] `parseChunked()` – Parse JSON that comes by chunks (e.g. FS readable stream or fetch response stream)
  9. - [x] `stringifyStream()` – Stringify stream (Node.js)
  10. - [x] `stringifyInfo()` – Get estimated size and other facts of JSON.stringify() without converting a value to string
  11. - [ ] **TBD** Support for circular references
  12. - [ ] **TBD** Binary representation [branch](https://github.com/discoveryjs/json-ext/tree/binary)
  13. - [ ] **TBD** WHATWG [Streams](https://streams.spec.whatwg.org/) support
  14. ## Install
  15. ```bash
  16. npm install @discoveryjs/json-ext
  17. ```
  18. ## API
  19. - [parseChunked(chunkEmitter)](#parsechunkedchunkemitter)
  20. - [stringifyStream(value[, replacer[, space]])](#stringifystreamvalue-replacer-space)
  21. - [stringifyInfo(value[, replacer[, space[, options]]])](#stringifyinfovalue-replacer-space-options)
  22. - [Options](#options)
  23. - [async](#async)
  24. - [continueOnCircular](#continueoncircular)
  25. - [version](#version)
  26. ### parseChunked(chunkEmitter)
  27. Works the same as [`JSON.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) but takes `chunkEmitter` instead of string and returns [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
  28. > NOTE: `reviver` parameter is not supported yet, but will be added in next releases.
  29. > NOTE: WHATWG streams aren't supported yet
  30. When to use:
  31. - It's required to avoid freezing the main thread during big JSON parsing, since this process can be distributed in time
  32. - Huge JSON needs to be parsed (e.g. >500MB on Node.js)
  33. - Needed to reduce memory pressure. `JSON.parse()` needs to receive the entire JSON before parsing it. With `parseChunked()` you may parse JSON as first bytes of it comes. This approach helps to avoid storing a huge string in the memory at a single time point and following GC.
  34. [Benchmark](https://github.com/discoveryjs/json-ext/tree/master/benchmarks#parse-chunked)
  35. Usage:
  36. ```js
  37. const { parseChunked } = require('@discoveryjs/json-ext');
  38. // as a regular Promise
  39. parseChunked(chunkEmitter)
  40. .then(data => {
  41. /* data is parsed JSON */
  42. });
  43. // using await (keep in mind that not every runtime has a support for top level await)
  44. const data = await parseChunked(chunkEmitter);
  45. ```
  46. Parameter `chunkEmitter` can be:
  47. - [`ReadableStream`](https://nodejs.org/dist/latest-v14.x/docs/api/stream.html#stream_readable_streams) (Node.js only)
  48. ```js
  49. const fs = require('fs');
  50. const { parseChunked } = require('@discoveryjs/json-ext');
  51. parseChunked(fs.createReadStream('path/to/file.json'))
  52. ```
  53. - Generator, async generator or function that returns iterable (chunks). Chunk might be a `string`, `Uint8Array` or `Buffer` (Node.js only):
  54. ```js
  55. const { parseChunked } = require('@discoveryjs/json-ext');
  56. const encoder = new TextEncoder();
  57. // generator
  58. parseChunked(function*() {
  59. yield '{ "hello":';
  60. yield Buffer.from(' "wor'); // Node.js only
  61. yield encoder.encode('ld" }'); // returns Uint8Array(5) [ 108, 100, 34, 32, 125 ]
  62. });
  63. // async generator
  64. parseChunked(async function*() {
  65. for await (const chunk of someAsyncSource) {
  66. yield chunk;
  67. }
  68. });
  69. // function that returns iterable
  70. parseChunked(() => ['{ "hello":', ' "world"}'])
  71. ```
  72. Using with [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API):
  73. ```js
  74. async function loadData(url) {
  75. const response = await fetch(url);
  76. const reader = response.body.getReader();
  77. return parseChunked(async function*() {
  78. while (true) {
  79. const { done, value } = await reader.read();
  80. if (done) {
  81. break;
  82. }
  83. yield value;
  84. }
  85. });
  86. }
  87. loadData('https://example.com/data.json')
  88. .then(data => {
  89. /* data is parsed JSON */
  90. })
  91. ```
  92. ### stringifyStream(value[, replacer[, space]])
  93. Works the same as [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify), but returns an instance of [`ReadableStream`](https://nodejs.org/dist/latest-v14.x/docs/api/stream.html#stream_readable_streams) instead of string.
  94. > NOTE: WHATWG Streams aren't supported yet, so function available for Node.js only for now
  95. Departs from JSON.stringify():
  96. - Outputs `null` when `JSON.stringify()` returns `undefined` (since streams may not emit `undefined`)
  97. - A promise is resolving and the resulting value is stringifying as a regular one
  98. - A stream in non-object mode is piping to output as is
  99. - A stream in object mode is piping to output as an array of objects
  100. When to use:
  101. - Huge JSON needs to be generated (e.g. >500MB on Node.js)
  102. - Needed to reduce memory pressure. `JSON.stringify()` needs to generate the entire JSON before send or write it to somewhere. With `stringifyStream()` you may send a result to somewhere as first bytes of the result appears. This approach helps to avoid storing a huge string in the memory at a single time point.
  103. - The object being serialized contains Promises or Streams (see Usage for examples)
  104. [Benchmark](https://github.com/discoveryjs/json-ext/tree/master/benchmarks#stream-stringifying)
  105. Usage:
  106. ```js
  107. const { stringifyStream } = require('@discoveryjs/json-ext');
  108. // handle events
  109. stringifyStream(data)
  110. .on('data', chunk => console.log(chunk))
  111. .on('error', error => consold.error(error))
  112. .on('finish', () => console.log('DONE!'));
  113. // pipe into a stream
  114. stringifyStream(data)
  115. .pipe(writableStream);
  116. ```
  117. Using Promise or ReadableStream in serializing object:
  118. ```js
  119. const fs = require('fs');
  120. const { stringifyStream } = require('@discoveryjs/json-ext');
  121. // output will be
  122. // {"name":"example","willSerializeResolvedValue":42,"fromFile":[1, 2, 3],"at":{"any":{"level":"promise!"}}}
  123. stringifyStream({
  124. name: 'example',
  125. willSerializeResolvedValue: Promise.resolve(42),
  126. fromFile: fs.createReadStream('path/to/file.json'), // support file content is "[1, 2, 3]", it'll be inserted as it
  127. at: {
  128. any: {
  129. level: new Promise(resolve => setTimeout(() => resolve('promise!'), 100))
  130. }
  131. }
  132. })
  133. // in case several async requests are used in object, it's prefered
  134. // to put fastest requests first, because in this case
  135. stringifyStream({
  136. foo: fetch('http://example.com/request_takes_2s').then(req => req.json()),
  137. bar: fetch('http://example.com/request_takes_5s').then(req => req.json())
  138. });
  139. ```
  140. Using with [`WritableStream`](https://nodejs.org/dist/latest-v14.x/docs/api/stream.html#stream_writable_streams) (Node.js only):
  141. ```js
  142. const fs = require('fs');
  143. const { stringifyStream } = require('@discoveryjs/json-ext');
  144. // pipe into a console
  145. stringifyStream(data)
  146. .pipe(process.stdout);
  147. // pipe into a file
  148. stringifyStream(data)
  149. .pipe(fs.createWriteStream('path/to/file.json'));
  150. // wrapping into a Promise
  151. new Promise((resolve, reject) => {
  152. stringifyStream(data)
  153. .on('error', reject)
  154. .pipe(stream)
  155. .on('error', reject)
  156. .on('finish', resolve);
  157. });
  158. ```
  159. ### stringifyInfo(value[, replacer[, space[, options]]])
  160. `value`, `replacer` and `space` arguments are the same as for `JSON.stringify()`.
  161. Result is an object:
  162. ```js
  163. {
  164. minLength: Number, // minimal bytes when values is stringified
  165. circular: [...], // list of circular references
  166. duplicate: [...], // list of objects that occur more than once
  167. async: [...] // list of async values, i.e. promises and streams
  168. }
  169. ```
  170. Example:
  171. ```js
  172. const { stringifyInfo } = require('@discoveryjs/json-ext');
  173. console.log(
  174. stringifyInfo({ test: true }).minLength
  175. );
  176. // > 13
  177. // that equals '{"test":true}'.length
  178. ```
  179. #### Options
  180. ##### async
  181. Type: `Boolean`
  182. Default: `false`
  183. Collect async values (promises and streams) or not.
  184. ##### continueOnCircular
  185. Type: `Boolean`
  186. Default: `false`
  187. Stop collecting info for a value or not whenever circular reference is found. Setting option to `true` allows to find all circular references.
  188. ### version
  189. The version of library, e.g. `"0.3.1"`.
  190. ## License
  191. MIT