-
Notifications
You must be signed in to change notification settings - Fork 161
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
``` |
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> |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is unnecessarily complicated. You don't need to track 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); |
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; | ||
} |
There was a problem hiding this comment.
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.