Dogs Chasing Squirrels

A software development blog

Tag Archives: TypeScript

Parsing streaming JSON in Typescript

0

In our servers, we will write large volumes of data out to the response stream directly in order to avoid the memory pressure of having all the data in memory at once.

In JavaScript/TypeScript, all though the fetch API supports streaming responses, there is no such thing, in the browser today, as a streaming JSON parser. As an exercise, I wrote a function that will wrap a request in an AsyncGenerator to yield objects of some type T where the response body consists of a JSON array of that type.
Note that the code yields parsed objects in batches. If every parsed object was yielded individually, the “for async” loop would create so many promises that the performance would degrade terribly.

  // Given a response that returns a JSON array of objects of type T, this
  // function returns an async generator that yields batches of objects of type T of a given batch size.
  // Note: If the batch size is too small, the many async calls reduce the performance
  async function* parseStreaming3(response: Response, batchSize: number = 100): AsyncGenerator {
    // If the response has no response body, stop.  This will only happen if something went wrong with the request.
    if (null === response.body) {
      console.warn(`Response has no body.`)
    } else {
      // The JSON object start character, '{'
      const START_OBJECT = 123;
      // The JSON object end character, '}'
      const END_OBJECT = 125;
      // Create a decoder
      const decoder = new TextDecoder('utf-8');
      // Get a streaming reader for the response body
      const reader = response.body.getReader();
      // Keep track of the object depth
      let depth = 0
      // If an object spans two chunks, the previous bytes that represent the end of the previous buffer
      let previousBytes: Uint8Array | undefined = undefined;
      // The start index of the current object
      let startIndex: number | undefined = undefined;
      // The current batch of items
      let batch = new Array()
      // eslint-disable-next-line no-constant-condition
      while (true) {
        // Get the bytes and whether the body is done
        const { done, value: bytes } = await reader.read();
        // If there's no value, stop.
        // If we have values...
        if (undefined !== bytes) {
          // noinspection JSIncompatibleTypesComparison
          // For each byte in the value...
          for (let i = 0; i < bytes.length; i++) {
            // Get the byte
            const byte = bytes[i];
            // If the byte is the start of a JSON object...
            if (START_OBJECT === byte) {
              // Increment the depth
              depth += 1;
              // If the depth is 1, meaning that this is a top-level object, set the start index
              if (1 === depth) {
                startIndex = i;
              }
              // If the byte is the end of an object...
            } else if (END_OBJECT === byte) {
              // If this is a top-level object...
              if (1 === depth) {
                // If there's a previous start index and previous bytes...
                if (undefined !== previousBytes) {
                  try {
                    // Combine the previous bytes with the current bytes
                    const json = decoder.decode(previousBytes)
                      + decoder.decode(bytes.subarray(0, i + 1));
                    // Parse the JSON into an object of the given type
                    const obj: T = JSON.parse(json);
                    // Add the parsed object to the batch
                    batch.push(obj);
                  } catch(e) {
                    console.warn(e)
                    console.log(` - previous json = `, decoder.decode(previousBytes))
                    console.log(` - json = `, decoder.decode(bytes.subarray(0, i + 1)))
                    // Stop
                    return
                  }
                  // Reset the previous bytes
                  previousBytes = undefined;
                  // If there's a start index...
                } else if (undefined !== startIndex) {
                  try {
                    // Get the JSON from the start index to the current index (inclusive)
                    const json = decoder.decode(bytes.subarray(startIndex, i + 1));
                    // Parse the JSON into an object of the given type
                    const obj: T = JSON.parse(json);
                    // Add the parsed object to the batch
                    batch.push(obj);
                    // Un-set the start index
                    startIndex = undefined;
                  } catch(e) {
                    console.warn(e)
                  }
                }
                // If the batch is at the batch size...
                if (batch.length === batchSize) {
                  // Yield the batch
                  yield batch;
                  // Reset the batch
                  batch = new Array()
                }
              }
              // Decrement the depth
              depth -= 1;
            }
          }
          // Because the start index is cleared at the end of each object,
          // if we're ending the loop with a start index, we must not have
          // encountered the end of the object, meaning that the object
          // spans (at least) two reads.
          if (undefined !== startIndex) {
            // If we have no previous bytes...
            if (undefined === previousBytes) {
              // Save the bytes from the start of the object to end of the buffer.
              // We'll combine this json with the next when we encounter the end of the
              // object in the next read.
              previousBytes = bytes.subarray(startIndex);
            } else {
              // There must not have been an end of the object in the previous read,
              // meaning that the read contains some middle section of an object
              // It happens sometimes, if we happen to get a particularly short read.
              // Combine the previous bytes with the current bytes, extending the data.
              const combinedBytes: Uint8Array = new Uint8Array(previousBytes.length + bytes.length);
              combinedBytes.set(previousBytes);
              combinedBytes.set(bytes, previousBytes.length);
              previousBytes = combinedBytes
            }
          }
        }
        // If we're at the end of the response body, stop.  There's no more data to read.
        if (done) {
          break;
        }
      }
      // If items remain in the batch, yield them
      if (batch.length > 0) {
        yield batch;
      }
    }
  }

React and .NET Core WebAPI with F# Part 1: React

0

I’m going to go through a step-by-step guide to getting React and .NET Core WebAPI working together. In this guide I’m going to try to document everything so there are no hidden steps and very little assumed knowledge.

In this first part, I’m just going to get a vanilla solution working with TypeScript and React.

Create the solution and project directories

We’re going to have the layout of a standard Visual Studio solution here, so create a folder for the solution, e.g. ReactWebApiDemo and then under that a folder for our web project, e.g. ReactWebApiDemo again.

Install npm

Most modern web projects use the Node.js package manager, npm, so the first step is to install it from either of the provided links. I’ll note that yarn is a possible alternative to npm and you’re welcome to try it instead, though the usage will be slightly different. At the time of writing, the npm version was 5.6.0.

First we need to initialize our project with

npm init

This gives the following:

PS C:\Projects\ReactWebApiDemo\ReactWebApiDemo> npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install ` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (reactwebapidemo)
version: (1.0.0)
description: React WebAPI Demo
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to C:\Projects\ReactWebApiDemo\ReactWebApiDemo\package.json:

{
  "name": "reactwebapidemo",
  "version": "1.0.0",
  "description": "React WebAPI Demo",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this ok? (yes) yes

After that, the syntax for installing packages that will end up in production code (like React) is:

npm install {module name}

or, for libraries that are used in development but will not end up in production:

npm install --save-dev {module name}

"–save-dev" can be replaced by "-D", e.g.

npm install -D {module name}

Install and Configure Webpack

The first thing we're going to set up is webpack. Webpack is a modular utility for minimizing and transforming our other files to make them production ready. We'll use it to:
* Convert TypeScript (.ts) files to JavaScript (.js) files
* Convert React TypeScript (.tsx) files to JavaScript (.js) files
* Less CSS (.less) files to CSS (.css) files.
* Pack our JavaScript and CSS files together with those of the third-party libraries we're using.
* Minimize and compress our JavaScript and CSS files.

The first step is to install webpack and its command-line interface, webpack-cli, which we can do with npm:

PS C:\Projects\ReactWebApiDemo\ReactWebApiDemo> npm install --save-dev webpack webpack-cli
[..................] - fetchMetadata: sill resolveWithNewModule webpack@4.10.2 checking installable status
  • webpack – The package and minimization utility.
  • webpack-cli – The webpack command line interface.

Webpack runs off a configuration file, webpack.config.js. This is the barest of configuration files to start with:

// Let us use the core webpack module as a library
const webpack = require( 'webpack' );
// Let us use the built-in webpack path module as a library
const path = require( 'path' );


module.exports = {
    // Entry: a.k.a. "Entry Point", the JS file that will used to build the JavaScript dependency graph
    // If not specified, src/index.js is the default.
    entry: "./src/index.js",
    // Output: Where we'll output the build files.
    output: {
        // Path: The directory to which we'll write transformed files
        path: path.resolve( __dirname, 'dist' ),
        // Filename: The name to which we'll write our bundled JavaScript.
        // If not specified, dist/main.js is the default.
        filename: 'main.js'
    },
    // The processing mode.  Accepted alues are "development", "production", or "none".
    mode: 'production'
}

You'll note that it needs an input file at "src/input.js" and an output directory at "dist", so create those in the web project.
After that we can run it with:

node .\node_modules\webpack\bin\webpack.js

e.g.

PS C:\Projects\ReactWebApiDemo\ReactWebApiDemo> node .\node_modules\webpack\bin\webpack.js
Hash: c4097b5edb272ec4b73c
Version: webpack 4.10.2
Time: 125ms
Built at: 2018-06-03 20:48:45
    Asset       Size  Chunks             Chunk Names
bundle.js  930 bytes       0  [emitted]  main
[0] ./src/index.js 0 bytes {0} [built]

We can make this easier on ourselves by setting this command up in the "scripts" section of package.json. E.g.

  "scripts": {
    "debug": "node ./node_modules/webpack/bin/webpack.js"
  },

Then:

npm run-script debug

or

npm run debug

We can make this even easier by putting the mode in the scripts and simplifying the scripts to just call "webpack" as in:

  "scripts": {
    "debug": "webpack --mode none",
    "dev": "webpack --mode development",
    "release": "webpack --mode production"
  },

We're going to make sure we have this working, so for now, change input.js to:

function test() {
}

Run npm run-script debug. It should create “dist\bundle.js”. Open it up. You should see some webpack overhead stuff and our test() method at the bottom.

One last change we can make is to let webpack know about common extensions so we can import files as just “import ‘./blah'” and not “import ‘./blah.js'”.
Add the following after the “module” section:

    resolve: {
        extensions: ['.js', '.ts', '.jsx', '.tsx', '.json']
    }

Setting up Less CSS

Run

npm install --save-dev less less-loader css-loader style-loader
  • less – The Less CSS library.
  • less-loader – The webpack module for Less-to-CSS conversion.
  • css-loader – The webpack module that allows us to import CSS into JavaScript.
  • style-loader – The webpack module that, with css-loader, lets us import styles into JavaScript.

Following the instructions on the less-loader site, we add this to webpack.config.js:

    // Define our modules here
    module : {
        rules: [
            { 
                test: /\.less$/, // Match all *.less files
                use: [{
                    loader: 'style-loader' // creates style nodes from JS strings
                  }, {
                    loader: 'css-loader' // translates CSS into CommonJS
                  }, {
                    loader: 'less-loader' // compiles Less to CSS
                  }]
            }
        ]
    }

Add a less file to src, e.g. “site.less”.

body {
    font-family: 'Times New Roman', Times, serif;
}

Have index.js import the style from the file, e.g.

import style from './site.less'

If you run webpack again, you’ll see bundle.js get updated.
You can test that the style is applied by creating a small HTML file, e.g.

<html>
    <head>
        <meta charset="utf-8">
        <title>Test</title>
        <a href="http://../dist/bundle.js">http://../dist/bundle.js</a>
    </head>
    <body>
        <h1>Test</h1>
    </body>
</html>

If you load the file in a browser, you’ll see the CSS is used.

Setting up TypeScript

I’m basically following the instructions here except we’re going to use awesome-typescript-loader instead. Once again, we start by installing the prerequisites. Run

npm install --save-dev typescript awesome-typescript-loader

TypeScript needs its own config file, tsconfig.json:

{
    "compilerOptions": {
        "outDir": "./dist/",
        "noImplicitAny": true,
        "module": "es6",
        "moduleResolution": "node",
        "target": "es5",
        "jsx": "react",
        "allowJs": true
    }
}

Note “moduleResolution”:”node”. This will allow us to use “import from ‘blah'” to import node modules.

In webpack.config.js we need the module definition:

            // Typescript
            {
                test: /\.tsx?$/, // Match *.ts and *.tsx
                use: 'awesome-typescript-loader', // Converts TypeScript to JavaScript
                exclude: /node_modules/ // Don't look in NPM's node_modules
            }

We can test it by putting a TypeScript file in the src folder. This is the TypeScript straight from the “TypeScript in 5 minutes” tutorial:

interface Person {
    firstName: string;
    lastName: string;
}

export function greeter(person: Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

Then reference it in index.js:

import greeter from './test.ts';

If you run webpack again, you’ll see the greeter code added to bundle.js.

Setting up React

Again, install react and react-dom with npm. No “–save-dev” this time – these are going in production!

npm install react react-dom

We’re also going to want to use babel to let us use EC6+ features in EC5 browsers. Start by installing babel (Dev only):

npm install --save-dev babel-core babel-loader 

We then install babel presets to tell it the plugins to set up.

npm install --save-dev babel-preset-env babel-preset-react

We need to set the babel loader up in our webpack.config.js:

            // Babel
            {
                test: /\.jsx?$/, // Match *.js and *.jsx
                use: 'babel-loader', // Converts ES2015+ JavaScript to browser-compatible JS
                exclude: /node_modules/ // Don't look in NPM's node_modules
            }

And babel needs its own configuration file, .babelrc, to tell it about the plugins:

{
    "presets": ["env", "react"]
}

Let’s test this out. We’ll change our index.js to include the React code given in the React tutorial:

import style from './site.less';
import React from "react";
import ReactDOM from "react-dom";

class ShoppingList extends React.Component {
    render() {
        return (
            <div>
                <h1>Shopping List for {this.props.name}</h1>
                <ul>
                    <li>Instagram</li>
                    <li>WhatsApp</li>
                    <li>Oculus</li>
                </ul>
            </div>
        );
    }
}

function renderShoppingList() {
    ReactDOM.render(
        <ShoppingList />,
        document.getElementById('shopping-list')
    );
}

window.onload = renderShoppingList;

If we load index.html we’ll see the React component render.

Let’s try loading files from a JSX. We’ll save this as ShoppingList.jsx:

import React from "react";
import ReactDOM from "react-dom";

class ShoppingList extends React.Component {
    render() {
      return (
        <div>
          <h1>Shopping List for {this.props.name}</h1>
          <ul>
            <li>A</li>
            <li>B</li>
            <li>C</li>
          </ul>
        </div>
      );
    }
  }

  module.exports = {
    renderShoppingList : function() {
      console.log( "renderShoppingList" );
      ReactDOM.render(
          <ShoppingList />,
          document.getElementById('shopping-list')
        );
    }
  }

Then modify our index.js like so:

import style from './site.less';
import React from "react";
import ReactDOM from "react-dom";

var shoppingList = require( "./ShoppingList" );

function test() {
    console.log( "test" );
    shoppingList.renderShoppingList();
}
window.onload = test;

And put the ID it requires in index.html:

    <body>
        <h1>Test</h1>
        <div id="shopping-list"></div>
    </body>

Now if we load our index.html in the browser we’ll see our React component.

Finally, let’s see if we can get this working with a TSX file.

For TypeScript to be able to import node modules properly we need to import the TypeScript type packages.

npm install --save-dev @types/react @types/react-dom

If we fail to do this we’ll get errors like “TS7016: Could not find a declaration file for module ‘react-dom'”.

Let’s make another component called AnotherComponent.tsx with the following code:

import * as React from 'react';
import * as ReactDOM from 'react-dom';

class AnotherComponent extends React.Component {
    public render() {
        return (
            <div>
                <h1>TSX</h1>
            </div>
        );
    }
}


export function renderAnotherComponent() {
    console.log("renderAnotherComponent");
    ReactDOM.render(
        <AnotherComponent />,
        document.getElementById('another-component')
    );
}

Now change index.js to call it:

<br />var shoppingList = require( "./ShoppingList" );
var anotherComponent = require( "./AnotherComponent" );

function test() {
    console.log( "test" );
    shoppingList.renderShoppingList();
    anotherComponent.renderAnotherComponent();
}
window.onload = test;

And put the ID it requires in index.html:

    <body>
        <h1>Test</h1>
        <div id="shopping-list"></div>
        <div id="another-component"></div>
    </body>

Now if we load index.html in a browser we’ll see our JSX component and our TSX component.

On to Part 2.