257 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			257 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # json-ext
 | ||
| 
 | ||
| [](https://www.npmjs.com/package/@discoveryjs/json-ext)
 | ||
| [](https://github.com/discoveryjs/json-ext/actions/workflows/ci.yml)
 | ||
| [](https://coveralls.io/github/discoveryjs/json-ext?)
 | ||
| [](https://www.npmjs.com/package/@discoveryjs/json-ext)
 | ||
| 
 | ||
| A set of utilities that extend the use of JSON. Designed to be fast and memory efficient
 | ||
| 
 | ||
| Features:
 | ||
| 
 | ||
| - [x] `parseChunked()` – Parse JSON that comes by chunks (e.g. FS readable stream or fetch response stream)
 | ||
| - [x] `stringifyStream()` – Stringify stream (Node.js)
 | ||
| - [x] `stringifyInfo()` – Get estimated size and other facts of JSON.stringify() without converting a value to string
 | ||
| - [ ] **TBD** Support for circular references
 | ||
| - [ ] **TBD** Binary representation [branch](https://github.com/discoveryjs/json-ext/tree/binary)
 | ||
| - [ ] **TBD** WHATWG [Streams](https://streams.spec.whatwg.org/) support
 | ||
| 
 | ||
| ## Install
 | ||
| 
 | ||
| ```bash
 | ||
| npm install @discoveryjs/json-ext
 | ||
| ```
 | ||
| 
 | ||
| ## API
 | ||
| 
 | ||
| - [parseChunked(chunkEmitter)](#parsechunkedchunkemitter)
 | ||
| - [stringifyStream(value[, replacer[, space]])](#stringifystreamvalue-replacer-space)
 | ||
| - [stringifyInfo(value[, replacer[, space[, options]]])](#stringifyinfovalue-replacer-space-options)
 | ||
|     - [Options](#options)
 | ||
|         - [async](#async)
 | ||
|         - [continueOnCircular](#continueoncircular)
 | ||
| - [version](#version)
 | ||
| 
 | ||
| ### parseChunked(chunkEmitter)
 | ||
| 
 | ||
| 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).
 | ||
| 
 | ||
| > NOTE: `reviver` parameter is not supported yet, but will be added in next releases.
 | ||
| > NOTE: WHATWG streams aren't supported yet
 | ||
| 
 | ||
| When to use:
 | ||
| - It's required to avoid freezing the main thread during big JSON parsing, since this process can be distributed in time
 | ||
| - Huge JSON needs to be parsed (e.g. >500MB on Node.js)
 | ||
| - 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.
 | ||
| 
 | ||
| [Benchmark](https://github.com/discoveryjs/json-ext/tree/master/benchmarks#parse-chunked)
 | ||
| 
 | ||
| Usage:
 | ||
| 
 | ||
| ```js
 | ||
| const { parseChunked } = require('@discoveryjs/json-ext');
 | ||
| 
 | ||
| // as a regular Promise
 | ||
| parseChunked(chunkEmitter)
 | ||
|     .then(data => {
 | ||
|         /* data is parsed JSON */
 | ||
|     });
 | ||
| 
 | ||
| // using await (keep in mind that not every runtime has a support for top level await)
 | ||
| const data = await parseChunked(chunkEmitter);
 | ||
| ```
 | ||
| 
 | ||
| Parameter `chunkEmitter` can be:
 | ||
| - [`ReadableStream`](https://nodejs.org/dist/latest-v14.x/docs/api/stream.html#stream_readable_streams) (Node.js only)
 | ||
| ```js
 | ||
| const fs = require('fs');
 | ||
| const { parseChunked } = require('@discoveryjs/json-ext');
 | ||
| 
 | ||
| parseChunked(fs.createReadStream('path/to/file.json'))
 | ||
| ```
 | ||
| - Generator, async generator or function that returns iterable (chunks). Chunk might be a `string`, `Uint8Array` or `Buffer` (Node.js only):
 | ||
| ```js
 | ||
| const { parseChunked } = require('@discoveryjs/json-ext');
 | ||
| const encoder = new TextEncoder();
 | ||
| 
 | ||
| // generator
 | ||
| parseChunked(function*() {
 | ||
|     yield '{ "hello":';
 | ||
|     yield Buffer.from(' "wor');    // Node.js only
 | ||
|     yield encoder.encode('ld" }'); // returns Uint8Array(5) [ 108, 100, 34, 32, 125 ]
 | ||
| });
 | ||
| 
 | ||
| // async generator
 | ||
| parseChunked(async function*() {
 | ||
|     for await (const chunk of someAsyncSource) {
 | ||
|         yield chunk;
 | ||
|     }
 | ||
| });
 | ||
| 
 | ||
| // function that returns iterable
 | ||
| parseChunked(() => ['{ "hello":', ' "world"}'])
 | ||
| ```
 | ||
| 
 | ||
| Using with [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API):
 | ||
| 
 | ||
| ```js
 | ||
| async function loadData(url) {
 | ||
|     const response = await fetch(url);
 | ||
|     const reader = response.body.getReader();
 | ||
| 
 | ||
|     return parseChunked(async function*() {
 | ||
|         while (true) {
 | ||
|             const { done, value } = await reader.read();
 | ||
| 
 | ||
|             if (done) {
 | ||
|                 break;
 | ||
|             }
 | ||
| 
 | ||
|             yield value;
 | ||
|         }
 | ||
|     });
 | ||
| }
 | ||
| 
 | ||
| loadData('https://example.com/data.json')
 | ||
|     .then(data => {
 | ||
|         /* data is parsed JSON */
 | ||
|     })
 | ||
| ```
 | ||
| 
 | ||
| ### stringifyStream(value[, replacer[, space]])
 | ||
| 
 | ||
| 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.
 | ||
| 
 | ||
| > NOTE: WHATWG Streams aren't supported yet, so function available for Node.js only for now
 | ||
| 
 | ||
| Departs from JSON.stringify():
 | ||
| - Outputs `null` when `JSON.stringify()` returns `undefined` (since streams may not emit `undefined`)
 | ||
| - A promise is resolving and the resulting value is stringifying as a regular one
 | ||
| - A stream in non-object mode is piping to output as is
 | ||
| - A stream in object mode is piping to output as an array of objects
 | ||
| 
 | ||
| When to use:
 | ||
| - Huge JSON needs to be generated (e.g. >500MB on Node.js)
 | ||
| - 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.
 | ||
| - The object being serialized contains Promises or Streams (see Usage for examples)
 | ||
| 
 | ||
| [Benchmark](https://github.com/discoveryjs/json-ext/tree/master/benchmarks#stream-stringifying)
 | ||
| 
 | ||
| Usage:
 | ||
| 
 | ||
| ```js
 | ||
| const { stringifyStream } = require('@discoveryjs/json-ext');
 | ||
| 
 | ||
| // handle events
 | ||
| stringifyStream(data)
 | ||
|     .on('data', chunk => console.log(chunk))
 | ||
|     .on('error', error => consold.error(error))
 | ||
|     .on('finish', () => console.log('DONE!'));
 | ||
| 
 | ||
| // pipe into a stream
 | ||
| stringifyStream(data)
 | ||
|     .pipe(writableStream);
 | ||
| ```
 | ||
| 
 | ||
| Using Promise or ReadableStream in serializing object:
 | ||
| 
 | ||
| ```js
 | ||
| const fs = require('fs');
 | ||
| const { stringifyStream } = require('@discoveryjs/json-ext');
 | ||
| 
 | ||
| // output will be
 | ||
| // {"name":"example","willSerializeResolvedValue":42,"fromFile":[1, 2, 3],"at":{"any":{"level":"promise!"}}}
 | ||
| stringifyStream({
 | ||
|     name: 'example',
 | ||
|     willSerializeResolvedValue: Promise.resolve(42),
 | ||
|     fromFile: fs.createReadStream('path/to/file.json'), // support file content is "[1, 2, 3]", it'll be inserted as it
 | ||
|     at: {
 | ||
|         any: {
 | ||
|             level: new Promise(resolve => setTimeout(() => resolve('promise!'), 100))
 | ||
|         }
 | ||
|     }
 | ||
| })
 | ||
| 
 | ||
| // in case several async requests are used in object, it's prefered
 | ||
| // to put fastest requests first, because in this case
 | ||
| stringifyStream({
 | ||
|     foo: fetch('http://example.com/request_takes_2s').then(req => req.json()),
 | ||
|     bar: fetch('http://example.com/request_takes_5s').then(req => req.json())
 | ||
| });
 | ||
| ```
 | ||
| 
 | ||
| Using with [`WritableStream`](https://nodejs.org/dist/latest-v14.x/docs/api/stream.html#stream_writable_streams) (Node.js only):
 | ||
| 
 | ||
| ```js
 | ||
| const fs = require('fs');
 | ||
| const { stringifyStream } = require('@discoveryjs/json-ext');
 | ||
| 
 | ||
| // pipe into a console
 | ||
| stringifyStream(data)
 | ||
|     .pipe(process.stdout);
 | ||
| 
 | ||
| // pipe into a file
 | ||
| stringifyStream(data)
 | ||
|     .pipe(fs.createWriteStream('path/to/file.json'));
 | ||
| 
 | ||
| // wrapping into a Promise
 | ||
| new Promise((resolve, reject) => {
 | ||
|     stringifyStream(data)
 | ||
|         .on('error', reject)
 | ||
|         .pipe(stream)
 | ||
|         .on('error', reject)
 | ||
|         .on('finish', resolve);
 | ||
| });
 | ||
| ```
 | ||
| 
 | ||
| ### stringifyInfo(value[, replacer[, space[, options]]])
 | ||
| 
 | ||
| `value`, `replacer` and `space` arguments are the same as for `JSON.stringify()`.
 | ||
| 
 | ||
| Result is an object:
 | ||
| 
 | ||
| ```js
 | ||
| {
 | ||
|     minLength: Number,  // minimal bytes when values is stringified
 | ||
|     circular: [...],    // list of circular references
 | ||
|     duplicate: [...],   // list of objects that occur more than once
 | ||
|     async: [...]        // list of async values, i.e. promises and streams
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| Example:
 | ||
| 
 | ||
| ```js
 | ||
| const { stringifyInfo } = require('@discoveryjs/json-ext');
 | ||
| 
 | ||
| console.log(
 | ||
|     stringifyInfo({ test: true }).minLength
 | ||
| );
 | ||
| // > 13
 | ||
| // that equals '{"test":true}'.length
 | ||
| ```
 | ||
| 
 | ||
| #### Options
 | ||
| 
 | ||
| ##### async
 | ||
| 
 | ||
| Type: `Boolean`  
 | ||
| Default: `false`
 | ||
| 
 | ||
| Collect async values (promises and streams) or not.
 | ||
| 
 | ||
| ##### continueOnCircular
 | ||
| 
 | ||
| Type: `Boolean`  
 | ||
| Default: `false`
 | ||
| 
 | ||
| Stop collecting info for a value or not whenever circular reference is found. Setting option to `true` allows to find all circular references.
 | ||
| 
 | ||
| ### version
 | ||
| 
 | ||
| The version of library, e.g. `"0.3.1"`.
 | ||
| 
 | ||
| ## License
 | ||
| 
 | ||
| MIT
 |