Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Random Values Generator Demonstration #844

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions demos/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ <h1>Streams Demos</h1>
<dl>
<dt><a href="append-child.html">Append child WritableStream</a>
<dd>Piping through a series of transforms to a custom writable stream that appends children to
the DOM.
the DOM.</dd>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The end tag is not needed, see https://html.spec.whatwg.org/multipage/grouping-content.html#the-dd-element.

I find it tidier to leave it out.


<dt><a href="streaming-element.html">Streaming element</a>
<dd>Using custom element to stream into a given DOM location.
<dt><a href="streaming-element.html">Streaming element</a></dt>
<dd>Using custom element to stream into a given DOM location.</dd>

<dt><a href="streaming-element-backpressure.html">Streaming element with backpressure</a>
<dd>A variation on the streaming element demo that adds backpressure to the element's writable
stream, decreasing jank.
stream, decreasing jank.</dd>

<dt><a href="./random-values-stream/">Random Values Stream</a></dt>
<dd>Piping a readable stream generating random values through a transform stream and then eventually writing it to the UI</dd>
</dl>

<p>Feel free to submit more demos by sending a pull request to the
Expand Down
12 changes: 12 additions & 0 deletions demos/random-values-stream/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Random Values Readable Stream

Random Values Cryptography Stream generates random values in a readable stream using the [getRandomValues](https://developer.mozilla.org/en-US/docs/Web/API/RandomSource/getRandomValues) in the [WebCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). The stream is then piped through a transform stream to make the output look prettier and then eventually written to a writable stream with an underlying sink that has relation to the User Interface.

## About the Code

A function called `pipeStream` initializes all the streams calling their respective methods along
with the arguments required and pipes the stream using the following code below

```js
readableStream.pipeThrough(transformStream).pipeTo(writableStream);
```
53 changes: 53 additions & 0 deletions demos/random-values-stream/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<html>
<head>
<title>BackPressure built stream</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css">
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<div class="header">
<h2>Random Cryptography Stream</h2>
<p>Demonstration for reading random values array generated through <a href="https://developer.mozilla.org/en/docs/Web/API/RandomSource/getRandomValues">WebCrypto getRandomValues function</a> from a <code>Readable Stream</code>, passing it through a <code>Transform Stream</code> which converts the chunk into a readable form and writing it to the UI in the <a href="#console">console</a> using a Writable Stream with backpressure implemented.

<div class="source-code">
<h2>Source Code</h2>
<p>Read the the <a href="https://github.com/whatwg/streams/tree/master/demos/random-values-stream/">README.md</a> file for explaination about the source code</p>
</div>
</div>

<!-- Contains all the demo's container logic -->
<div class="demo-container">
<div class="controls">
<div class="pipe-controls">
<h3>Readable Stream Pipe</h3>
<p>Pipe readable stream through a transform stream to eventually write to a writable stream with an underlying sink that writes to the Web UI. Click the button below to start piping through the stream</p>
<button id="pipe-through">Pipe Through Transform</button>
</div>
</div>

<!--
Div tag where console values are going to appear after a certain period of
time, requestAnimationFrame is also called to handle animations effectively,
if the amount of text to be written is a lot more
-->
<div class="output">
<div class="stream-status"></div>

<h4>Status</h4>
<div class="status-container">
Logs the status of the Readable and Writable Streams and data written to them
</div>

<h3>Output</h3>
<!-- Container for the output provided by the writable stream -->
<div class="output-container">
Output for the underlying sink will appear here
</div>
</div>
</div>


<script src="../transforms/transform-stream-polyfill.js" type="text/javascript"></script>
<script src="index.js" type="text/javascript"></script>
</body>
</html>
217 changes: 217 additions & 0 deletions demos/random-values-stream/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/**
* Creates random values with the help of WebCrypto API
*/
function createRandomValuesStream(numberOfBytes = 10, valueInterval = 1000, maxValues = null) {
const cqs = new CountQueuingStrategy({ highWaterMark: 4 });
const readableStream = new ReadableStream({
totalEnqueuedItemsCount: 0,
interval: null,

start(controller) {
logStatusText('`start` method of the readable stream called')
this.startValueInterval(controller);
},

/**
* Starting the random values generation again after a certain period
* @param {*} controller
*/
async pull(controller) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessarily complicated. You don't need to track desiredSize yourself. As long as you have called enqueue() you can rely on the ReadableStream to call you back when there is no backpressure.

It would be simpler to do something like

pull(controller) {
  if (maxValues && this.totalEnqueuedItemsCount >= maxValues) {
    controller.close();
    return;
  }
  let resolve;
  const promise = new Promise(r => {
    resolve = r;
  });
  setTimeout(() => {
    controller.enqueue(randomValuesUint8Array(20));
    ++this.totalEnqueuedItemsCount;
    resolve();
  }, valueInterval);
  return promise;
}

(untested).

if (controller.desiredSize > 2 && !this.interval) {
this.startValueInterval(controller);
}
},

async close(controller) {
logStatusText('`close` method of the readable stream called');
this.clearValueInterval();
controller.close();
return;
},

async cancel() {
logStatusText('`cancel` method of the readable stream called')
this.clearValueInterval();
},

throwFinalError(error) {
console.log('Errored out');
console.error(error);

this.clearValueInterval();
},

startValueInterval(controller) {
if (this.interval) {
return;
}

this.interval = setInterval(() => {
try {
controller.enqueue(randomValuesUint8Array(20));
this.totalEnqueuedItemsCount++;
this.checkBackpressureSignal(controller);

// Close the stream and clear the interval
if (maxValues && this.totalEnqueuedItemsCount >= maxValues) {
return this.close(controller);
}
} catch (error) {
this.throwFinalError(error);
}
}, valueInterval);
},

/**
* Clears the value interval stored in this.interval reference
*/
clearValueInterval() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
},

/**
* Checks a backpressure signal and clears the interval
* not enqueuing any more values
*
* @param {*} controller
* @param {*} interval
*/
checkBackpressureSignal(controller, interval) {
if (controller.desiredSize <= 0) {
this.clearValueInterval();
}
}
}, cqs);

return readableStream;
}

/**
* Creates a random values Uint8Array
* @param {*} numberOfBytes
*/
function randomValuesUint8Array(numberOfBytes) {
const uint8Array = new Uint8Array(numberOfBytes);
return window.crypto.getRandomValues(uint8Array);
}

/**
* Create a writable stream to display the output with a builtin backpressure
*/
function createOutputWritableStream(parentElement) {
/**
* Equivalent to
*
* const cqs = new CountQueuingStrategy({
* highWaterMark: 3,
* });
*/
const queuingStrategy = {
highWaterMark: 3,
size() { return 1; }
}

const writable = new WritableStream({
async write(chunk, controller) {
try {
await writeChunk(chunk);
return;
} catch (error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary to catch the exception. If write() throws, the stream will errored automatically.

return this.finalErrorHandler(error, controller);
}
},

finalErrorHandler(error, controller) {
controller.error(error);
logStatusText('Error occured in the writable stream');
return error;
},

close() {
logStatusText('Closing the stream');
console.log('Stream closed');
}
});

/**
* Writes a chunk to the span and appends it to the parent element
* @param {*} chunk
*/
async function writeChunk(chunk) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function doesn't need to be async as it performs no asynchronous operations. Also, as it is only called within write(), it might as well just be included in that function.

const containerElement = document.createElement('div');
containerElement.className = 'output-chunk';
containerElement.textContent = chunk;
parentElement.appendChild(containerElement);
return containerElement;
}

return writable;
}

function createArrayToStringTransform() {
const transformStream = new TransformStream({
transform (chunk, controller) {
controller.enqueue(`${chunk.constructor.name}(${chunk.join(', ')})`);
}
});

return transformStream;
}

/**
* Logs text regarding a status which is apart from the
* data written to the underlying sink and is related to status
* of the readable and writable streams
*/

const statusContainer = document.querySelector('.output .status-container');

/**
* Logs status text
* @param {*} statusText
*/
function logStatusText(statusText) {
const divElement = document.createElement('div');
divElement.className = 'status-chunk';
divElement.textContent = statusText;

statusContainer.appendChild(divElement);
}

/**
* Demo related code
*/
async function pipeThroughHandler() {
const outputContainer = document.querySelector('.output .output-container');
const pipeThroughButton = document.querySelector('.pipe-controls #pipe-through');
outputContainer.innerHTML = statusContainer.innerHTML = '';

try {
pipeThroughButton.disabled = true;
logStatusText('Started writing to the stream');
await pipeStream(outputContainer);
} catch (error) {
console.error(error);
}

logStatusText('Done writing to the stream');
pipeThroughButton.disabled = false;
}

async function pipeStream(parentElement) {
const readableStream = createRandomValuesStream(10, 1000, 10);
const writableStream = createOutputWritableStream(parentElement);
const transformStream = createArrayToStringTransform();

return readableStream.pipeThrough(transformStream).pipeTo(writableStream);
}

function initDemo() {
const pipeThroughButton = document.querySelector('.pipe-controls #pipe-through');
pipeThroughButton.addEventListener('click', pipeThroughHandler);
}

initDemo();
11 changes: 11 additions & 0 deletions demos/random-values-stream/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
body {
margin: 2em auto ;
max-width: 970px;
}

.output .output-container {
background-color: yellow;
overflow: auto;
padding: 10px;
max-height: 400px;
}