Skip to content

Instantly share code, notes, and snippets.

@bgoonz
Last active December 30, 2022 00:35
Show Gist options
  • Save bgoonz/d928f0deb01ce93bfd2a6cd70dbc05a6 to your computer and use it in GitHub Desktop.
Save bgoonz/d928f0deb01ce93bfd2a6cd70dbc05a6 to your computer and use it in GitHub Desktop.
// Find all occurrences of a value x in an array a and return an array
// of matching indexes
function findall(a, x) {
let results = [], // The array of indexes we'll return
len = a.length, // The length of the array to be searched
pos = 0; // The position to search from
while (pos < len) {
// While more elements to search...
pos = a.indexOf(x, pos); // Search
if (pos === -1) break; // If nothing found, we're done.
results.push(pos); // Otherwise, store index in array
pos = pos + 1; // And start next search at next element
}
return results; // Return array of indexes
}
// Return the document's cookies as a Map object.
// Assume that cookie values are encoded with encodeURIComponent().
function getCookies() {
let cookies = new Map(); // The object we will return
let all = document.cookie; // Get all cookies in one big string
let list = all.split("; "); // Split into individual name/value pairs
for (let cookie of list) {
// For each cookie in that list
if (!cookie.includes("=")) continue; // Skip if there is no = sign
let p = cookie.indexOf("="); // Find the first = sign
let name = cookie.substring(0, p); // Get cookie name
let value = cookie.substring(p + 1); // Get cookie value
value = decodeURIComponent(value); // Decode the value
cookies.set(name, value); // Remember cookie name and value
}
return cookies;
}
const http = require("http");
function getJSON(url) {
// Create and return a new Promise
return new Promise((resolve, reject) => {
// Start an HTTP GET request for the specified URL
request = http.get(url, (response) => {
// called when response starts
// Reject the Promise if the HTTP status is wrong
if (response.statusCode !== 200) {
reject(new Error(`HTTP status ${response.statusCode}`));
response.resume(); // so we don't leak memory
}
// And reject if the response headers are wrong
else if (response.headers["content-type"] !== "application/json") {
reject(new Error("Invalid content-type"));
response.resume(); // don't leak memory
} else {
// Otherwise, register events to read the body of the response
let body = "";
response.setEncoding("utf-8");
response.on("data", (chunk) => {
body += chunk;
});
response.on("end", () => {
// When the response body is complete, try to parse it
try {
let parsed = JSON.parse(body);
// If it parsed successfully, fulfill the Promise
resolve(parsed);
} catch (e) {
// If parsing failed, reject the Promise
reject(e);
}
});
}
});
// We also reject the Promise if the request fails before we
// even get a response (such as when the network is down)
request.on("error", (error) => {
reject(error);
});
});
}
const https = require("https");
// Read the text content of the URL and asynchronously pass it to the callback.
function getText(url, callback) {
// Start an HTTP GET request for the URL
request = https.get(url);
// Register a function to handle the "response" event.
request.on("response", (response) => {
// The response event means that response headers have been received
let httpStatus = response.statusCode;
// The body of the HTTP response has not been received yet.
// So we register more event handlers to to be called when it arrives.
response.setEncoding("utf-8"); // We're expecting Unicode text
let body = ""; // which we will accumulate here.
// This event handler is called when a chunk of the body is ready
response.on("data", (chunk) => {
body += chunk;
});
// This event handler is called when the response is complete
response.on("end", () => {
if (httpStatus === 200) {
// If the HTTP response was good
callback(null, body); // Pass response body to the callback
} else {
// Otherwise pass an error
callback(httpStatus, null);
}
});
});
// We also register an event handler for lower-level network errors
request.on("error", (err) => {
callback(err, null);
});
}
class Glob {
constructor(glob) {
this.glob = glob;
// We implement glob matching using RegExp internally.
// ? matches any one character except /, and * matches zero or more
// of those characters. We use capturing groups around each.
let regexpText = glob.replace("?", "([^/])").replace("*", "([^/]*)");
// We use the u flag to get Unicode-aware matching.
// Globs are intended to match entire strings, so we use the ^ and $
// anchors and do not implement search() or matchAll() since they
// are not useful with patterns like this.
this.regexp = new RegExp(`^${regexpText}$`, "u");
}
toString() {
return this.glob;
}
[Symbol.search](s) {
return s.search(this.regexp);
}
[Symbol.match](s) {
return s.match(this.regexp);
}
[Symbol.replace](s, replacement) {
return s.replace(this.regexp, replacement);
}
}
let pattern = new Glob("docs/*.txt");
"docs/js.txt".search(pattern); // => 0: matches at character 0
"docs/js.htm".search(pattern); // => -1: does not match
let match = "docs/js.txt".match(pattern);
match[0]; // => "docs/js.txt"
match[1]; // => "js"
match.index; // => 0
"docs/js.txt".replace(pattern, "web/$1.htm"); // => "web/js.htm"
function glob(strings, ...values) {
// Assemble the strings and values into a single string
let s = strings[0];
for (let i = 0; i < values.length; i++) {
s += values[i] + strings[i + 1];
}
// Return a parsed representation of that string
return new Glob(s);
}
let root = "/tmp";
let filePattern = glob`${root}/*.html`; // A RegExp alternative
"/tmp/test.html".match(filePattern)[1]; // => "test"
const stream = require("stream");
class GrepStream extends stream.Transform {
constructor(pattern) {
super({ decodeStrings: false }); // Don't convert strings back to buffers
this.pattern = pattern; // The regular expression we want to match
this.incompleteLine = ""; // Any remnant of the last chunk of data
}
// This method is invoked when there is a string ready to be
// transformed. It should pass transformed data to the specified
// callback function. We expect string input so this stream should
// only be connected to readable streams that have had
// setEncoding() called on them.
_transform(chunk, encoding, callback) {
if (typeof chunk !== "string") {
callback(new Error("Expected a string but got a buffer"));
return;
}
// Add the chunk to any previously incomplete line and break
// everything into lines
let lines = (this.incompleteLine + chunk).split("\n");
// The last element of the array is the new incomplete line
this.incompleteLine = lines.pop();
// Find all matching lines
let output = lines // Start with all complete lines,
.filter((l) => this.pattern.test(l)) // filter them for matches,
.join("\n"); // and join them back up.
// If anything matched, add a final newline
if (output) {
output += "\n";
}
// Always call the callback even if there is no output
callback(null, output);
}
// This is called right before the stream is closed.
// It is our chance to write out any last data.
_flush(callback) {
// If we still have an incomplete line, and it matches
// pass it to the callback
if (this.pattern.test(this.incompleteLine)) {
callback(null, this.incompleteLine + "\n");
}
}
}
// Now we can write a program like 'grep' with this class.
let pattern = new RegExp(process.argv[2]); // Get a RegExp from command line.
process.stdin // Start with standard input,
.setEncoding("utf8") // read it as Unicode strings,
.pipe(new GrepStream(pattern)) // pipe it to our GrepStream,
.pipe(process.stdout) // and pipe that to standard out.
.on("error", () => process.exit()); // Exit gracefully if stdout closes.
const fs = require("fs");
const zlib = require("zlib");
function gzip(filename, callback) {
// Create the streams
let source = fs.createReadStream(filename);
let destination = fs.createWriteStream(filename + ".gz");
let gzipper = zlib.createGzip();
// Set up the pipeline
source
.on("error", callback) // call callback on read error
.pipe(gzipper)
.pipe(destination)
.on("error", callback) // call callback on write error
.on("finish", callback); // call callback when writing is complete
}
const fs = require("fs");
const crypto = require("crypto");
// Compute a sha256 hash of the contents of the named file and pass the
// hash (as a string) to the specified error-first callback function.
function sha256(filename, callback) {
let input = fs.createReadStream(filename); // The data stream.
let hasher = crypto.createHash("sha256"); // For computing the hash.
input.on("readable", () => {
// When there is data ready to read
let chunk;
while ((chunk = input.read())) {
// Read a chunk, and if non-null,
hasher.update(chunk); // pass it to the hasher,
} // and keep looping until not readable
});
input.on("end", () => {
// At the end of the stream,
let hash = hasher.digest("hex"); // compute the hash,
callback(null, hash); // and pass it to the callback.
});
input.on("error", callback); // On error, call callback
}
// Here's a simple command-line utility to compute the hash of a file
sha256(process.argv[2], (err, hash) => {
// Pass filename from command line.
if (err) {
// If we get an error
console.error(err.toString()); // print it as an error.
} else {
// Otherwise,
console.log(hash); // print the hash string.
}
});
/**
* A Set-like class that keeps track of how many times a value has
* been added. Call add() and remove() like you would for a Set, and
* call count() to find out how many times a given value has been added.
* The default iterator yields the values that have been added at least
* once. Use entries() if you want to iterate [value, count] pairs.
*/
class Histogram {
// To initialize, we just create a Map object to delegate to
constructor() {
this.map = new Map();
}
// For any given key, the count is the value in the Map, or zero
// if the key does not appear in the Map.
count(key) {
return this.map.get(key) || 0;
}
// The Set-like method has() returns true if the count is non-zero
has(key) {
return this.count(key) > 0;
}
// The size of the histogram is just the number of entries in the Map.
get size() {
return this.map.size;
}
// To add a key, just increment its count in the Map.
add(key) {
this.map.set(key, this.count(key) + 1);
}
// Deleting a key is a little trickier because we have to delete
// the key from the Map if the count goes back down to zero.
delete(key) {
let count = this.count(key);
if (count === 1) {
this.map.delete(key);
} else if (count > 1) {
this.map.set(key, count - 1);
}
}
// Iterating a Histogram just returns the keys stored in it
[Symbol.iterator]() {
return this.map.keys();
}
// These other iterator methods just delegate to the Map object
keys() {
return this.map.keys();
}
values() {
return this.map.values();
}
entries() {
return this.map.entries();
}
}
function html(strings, ...values) {
// Convert each value to a string and escape special HTML characters
let escaped = values.map((v) =>
String(v)
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
.replace("'", "&#39;")
);
// Return the concatenated strings and escaped values
let result = strings[0];
for (let i = 0; i < escaped.length; i++) {
result += escaped[i] + strings[i + 1];
}
return result;
}
let operator = "<";
html`<b>x ${operator} y</b>`; // => "<b>x &lt; y</b>"
let kind = "game",
name = "D&D";
html`<div class="${kind}">${name}</div>`; // =>'<div class="game">D&amp;D</div>'
// We use a Proxy to create an object that appears to have every
// possible property, with the value of each property equal to its name
let identity = new Proxy(
{},
{
// Every property has its own name as its value
get(o, name, target) {
return name;
},
// Every property name is defined
has(o, name) {
return true;
},
// There are too many properties to enumerate, so we just throw
ownKeys(o) {
throw new RangeError("Infinite number of properties");
},
// All properties exist and are not writable, configurable or enumerable.
getOwnPropertyDescriptor(o, name) {
return {
value: name,
enumerable: false,
writable: false,
configurable: false,
};
},
// All properties are read-only so they can't be set
set(o, name, value, target) {
return false;
},
// All properties are non-configurable, so they can't be deleted
deleteProperty(o, name) {
return false;
},
// All properties exist and are non-configurable so we can't define more
defineProperty(o, name, desc) {
return false;
},
// In effect, this means that the object is not extensible
isExtensible(o) {
return false;
},
// All properties are already defined on this object, so it couldn't
// inherit anything even if it did have a prototype object.
getPrototypeOf(o) {
return null;
},
// The object is not extensible, so we can't change the prototype
setPrototypeOf(o, proto) {
return false;
},
}
);
identity.x; // => "x"
identity.toString; // => "toString"
identity[0]; // => "0"
identity.x = 1; // Setting properties has no effect
identity.x; // => "x"
delete identity.x; // => false: can't delete properties either
identity.x; // => "x"
Object.keys(identity); // !RangeError: can't list all the keys
for (let p of identity); // !RangeError
// Asynchronously load and execute a script from a specified URL
// Returns a Promise that resolves when the script has loaded.
function importScript(url) {
return new Promise((resolve, reject) => {
let s = document.createElement("script"); // Create a <script> element
s.onload = () => {
resolve();
}; // Resolve promise when loaded
s.onerror = (e) => {
reject(e);
}; // Reject on failure
s.src = url; // Set the script URL
document.head.append(s); // Add <script> to document
});
}
customElements.define(
"inline-circle",
class InlineCircle extends HTMLElement {
// The browser calls this method when an <inline-circle> element
// is inserted into the document. There is also a disconnectedCallback()
// that we don't need in this example.
connectedCallback() {
// Set the styles needed to create circles
this.style.display = "inline-block";
this.style.borderRadius = "50%";
this.style.border = "solid black 1px";
this.style.transform = "translateY(10%)";
// If there is not already a size defined, set a default size
// that is based on the current font size.
if (!this.style.width) {
this.style.width = "0.8em";
this.style.height = "0.8em";
}
}
// The static observedAttributes property specifies which attributes
// we want to be notified about changes to. (We use a getter here since
// we can only use "static" with methods.)
static get observedAttributes() {
return ["diameter", "color"];
}
// This callback is invoked when one of the attributes listed above
// changes, either when the custom element is first parsed, or later.
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "diameter":
// If the diameter attribute changes, update the size styles
this.style.width = newValue;
this.style.height = newValue;
break;
case "color":
// If the color attribute changes, update the color styles
this.style.backgroundColor = newValue;
break;
}
}
// Define JavaScript properties that correspond to the element's
// attributes. These getters and setters just get and set the underlying
// attributes. If a JavaScript property is set, that sets the attribute
// which triggers a call to attributeChangedCallback() which updates
// the element styles.
get diameter() {
return this.getAttribute("diameter");
}
set diameter(diameter) {
this.setAttribute("diameter", diameter);
}
get color() {
return this.getAttribute("color");
}
set color(color) {
this.setAttribute("color", color);
}
}
);
// Determine if o is an array-like object.
// Strings and functions have numeric length properties, but are
// excluded by the typeof test. In client-side JavaScript, DOM text
// nodes have a numeric length property, and may need to be excluded
// with an additional o.nodeType !== 3 test.
function isArrayLike(o) {
if (
o && // o is not null, undefined, etc.
typeof o === "object" && // o is an object
Number.isFinite(o.length) && // o.length is a finite number
o.length >= 0 && // o.length is non-negative
Number.isInteger(o.length) && // o.length is an integer
o.length < 4294967295
) {
// o.length < 2^32 - 1
return true; // Then o is array-like.
} else {
return false; // Otherwise it is not.
}
}
// Connect to the joke port (6789) on the server named on the command line
let socket = require("net").createConnection(6789, process.argv[2]);
socket.pipe(process.stdout); // Pipe data from the socket to stdout
process.stdin.pipe(socket); // Pipe data from stdin to the socket
socket.on("close", () => process.exit()); // Quit when the socket closes.
// A TCP server that delivers interactive knock-knock jokes on port 6789.
// (Why is six afraid of seven? Because seven ate nine!)
const net = require("net");
const readline = require("readline");
// Create a Server object and start listening for connections
let server = net.createServer();
server.listen(6789, () => console.log("Delivering laughs on port 6789"));
// When a client connects, tell them a knock-knock joke.
server.on("connection", (socket) => {
tellJoke(socket)
.then(() => socket.end()) // When the joke is done, close the socket.
.catch((err) => {
console.error(err); // Log any errors that occur,
socket.end(); // but still close the socket!
});
});
// These are all the jokes we know.
const jokes = {
Boo: "Don't cry...it's only a joke!",
Lettuce: "Let us in! It's freezing out here!",
"A little old lady": "Wow, I didn't know you could yodel!",
};
// Interactively perform a knock-knock joke over this socket, without blocking.
async function tellJoke(socket) {
// Pick one of the jokes at random
let randomElement = (a) => a[Math.floor(Math.random() * a.length)];
let who = randomElement(Object.keys(jokes));
let punchline = jokes[who];
// Use the readline module to read the user's input one line at a time.
let lineReader = readline.createInterface({
input: socket,
output: socket,
prompt: ">> ",
});
// A utility function to output a line of text to the client
// and then (by default) display a prompt.
function output(text, prompt = true) {
socket.write(`${text}\r\n`);
if (prompt) lineReader.prompt();
}
// Knock-knock jokes have a call-and-response structure.
// We expect different input from the user at different stages and
// take different action when we get that input at different stages.
let stage = 0;
// Start the knock-knock joke off in the traditional way.
output("Knock knock!");
// Now read lines asynchronously from the client until the joke is done.
for await (let inputLine of lineReader) {
if (stage === 0) {
if (inputLine.toLowerCase() === "who's there?") {
// If the user gives the right response at stage 0
// then tell the first part of the joke and go to stage 1.
output(who);
stage = 1;
} else {
// Otherwise teach the user how to do knock-knock jokes.
output('Please type "Who\'s there?".');
}
} else if (stage === 1) {
if (inputLine.toLowerCase() === `${who.toLowerCase()} who?`) {
// If the user's response is correct at stage 1, then
// deliver the punchline and return since the joke is done.
output(`${punchline}`, false);
return;
} else {
// Make the user play along.
output(`Please type "${who} who?".`);
}
}
}
}
let deg = Math.PI / 180; // For converting degrees to radians
// Draw a level-n Koch snowflake fractal on the canvas context c,
// with lower-left corner at (x,y) and side length len.
function snowflake(c, n, x, y, len) {
c.save(); // Save current transformation
c.translate(x, y); // Translate origin to starting point
c.moveTo(0, 0); // Begin a new subpath at the new origin
leg(n); // Draw the first leg of the snowflake
c.rotate(-120 * deg); // Now rotate 120 degrees counterclockwise
leg(n); // Draw the second leg
c.rotate(-120 * deg); // Rotate again
leg(n); // Draw the final leg
c.closePath(); // Close the subpath
c.restore(); // And restore original transformation
// Draw a single leg of a level-n Koch snowflake.
// This function leaves the current point at the end of the leg it has
// drawn and translates the coordinate system so the current point is (0,0).
// This means you can easily call rotate() after drawing a leg.
function leg(n) {
c.save(); // Save the current transformation
if (n === 0) {
// Nonrecursive case:
c.lineTo(len, 0); // Just draw a horizontal line
} // _ _
else {
// Recursive case: draw 4 sub-legs like: \/
c.scale(1 / 3, 1 / 3); // Sub-legs are 1/3 the size of this leg
leg(n - 1); // Recurse for the first sub-leg
c.rotate(60 * deg); // Turn 60 degrees clockwise
leg(n - 1); // Second sub-leg
c.rotate(-120 * deg); // Rotate 120 degrees back
leg(n - 1); // Third sub-leg
c.rotate(60 * deg); // Rotate back to our original heading
leg(n - 1); // Final sub-leg
}
c.restore(); // Restore the transformation
c.translate(len, 0); // But translate to make end of leg (0,0)
}
}
let c = document.querySelector("canvas").getContext("2d");
snowflake(c, 0, 25, 125, 125); // A level-0 snowflake is a triangle
snowflake(c, 1, 175, 125, 125); // A level-1 snowflake is a 6-sided star
snowflake(c, 2, 325, 125, 125); // etc.
snowflake(c, 3, 475, 125, 125);
snowflake(c, 4, 625, 125, 125); // A level-4 snowflake looks like a snowflake!
c.stroke(); // Stroke this very complicated path
const fs = require("fs");
const path = require("path");
async function listDirectory(dirpath) {
let dir = await fs.promises.opendir(dirpath);
for await (let entry of dir) {
let name = entry.name;
if (entry.isDirectory()) {
name += "/"; // Add a trailing slash to subdirectories
}
let stats = await fs.promises.stat(path.join(dirpath, name));
let size = stats.size;
console.log(String(size).padStart(10), name);
}
}
// If the integer 0x00000001 is arranged in memory as 01 00 00 00, then
// we're on a little-endian platform. On a big-endian platform, we'd get
// bytes 00 00 00 01 instead.
let littleEndian = new Int8Array(new Int32Array([1]).buffer)[0] === 1;
/*
* Return a Proxy object that wraps o, delegating all operations to
* that object after logging each operation. objname is a string that
* will appear in the log messages to identify the object. If o has own
* properties whose values are objects or functions, then if you query
* the value of those properties, you'll get a loggingProxy back, so that
* logging behavior of this proxy is "contagious".
*/
function loggingProxy(o, objname) {
// Define handlers for our logging Proxy object.
// Each handler logs a message and then delegates to the target object.
const handlers = {
// This handler is a special case because for own properties
// whose value is an object or function, it returns a proxy rather
// than returning the value itself.
get(target, property, receiver) {
// Log the get operation
console.log(`Handler get(${objname},${property.toString()})`);
// Use the Reflect API to get the property value
let value = Reflect.get(target, property, receiver);
// If the property is an own property of the target and
// the value is an object or function then return a Proxy for it.
if (
Reflect.ownKeys(target).includes(property) &&
(typeof value === "object" || typeof value === "function")
) {
return loggingProxy(value, `${objname}.${property.toString()}`);
}
// Otherwise return the value unmodified.
return value;
},
// There is nothing special about the following three methods:
// they log the operation and delegate to the target object.
// They are a special case simply so we can avoid logging the
// receiver object which can cause infinite recursion.
set(target, prop, value, receiver) {
console.log(`Handler set(${objname},${prop.toString()},${value})`);
return Reflect.set(target, prop, value, receiver);
},
apply(target, receiver, args) {
console.log(`Handler ${objname}(${args})`);
return Reflect.apply(target, receiver, args);
},
construct(target, args, receiver) {
console.log(`Handler ${objname}(${args})`);
return Reflect.construct(target, args, receiver);
},
};
// We can automatically generate the rest of the handlers.
// Metaprogramming FTW!
Reflect.ownKeys(Reflect).forEach((handlerName) => {
if (!(handlerName in handlers)) {
handlers[handlerName] = function (target, ...args) {
// Log the operation
console.log(`Handler ${handlerName}(${objname},${args})`);
// Delegate the operation
return Reflect[handlerName](target, ...args);
};
}
});
// Return a proxy for the object using these logging handlers
return new Proxy(o, handlers);
}
/*
* This class represents a subrectangle of a canvas or image. We use Tiles to
* divide a canvas into regions that can be processed independently by Workers.
*/
class Tile {
constructor(x, y, width, height) {
this.x = x; // The properties of a Tile object
this.y = y; // represent the position and size
this.width = width; // of the tile within a larger
this.height = height; // rectangle.
}
// This static method is a generator that divides a rectangle of the
// specified width and height into the specified number of rows and
// columns and yields numRows*numCols Tile objects to cover the rectangle.
static *tiles(width, height, numRows, numCols) {
let columnWidth = Math.ceil(width / numCols);
let rowHeight = Math.ceil(height / numRows);
for (let row = 0; row < numRows; row++) {
let tileHeight =
row < numRows - 1
? rowHeight // height of most rows
: height - rowHeight * (numRows - 1); // height of last row
for (let col = 0; col < numCols; col++) {
let tileWidth =
col < numCols - 1
? columnWidth // width of most columns
: width - columnWidth * (numCols - 1); // and last column
yield new Tile(
col * columnWidth,
row * rowHeight,
tileWidth,
tileHeight
);
}
}
}
}
/*
* This class represents a pool of workers, all running the same code. The
* worker code you specify must respond to each message it receives by
* performing some kind of computation and then posting a single message with
* the result of that computation.
*
* Given a WorkerPool and message that represents work to be performed, simply
* call addWork(), with the message as an argument. If there is a Worker
* object that is currently idle, the message will be posted to that worker
* immediately. If there are no idle Worker objects, the message will be
* queued and will be posted to a Worker when one becomes available.
*
* addWork() returns a Promise, which will resolve with the message recieved
* from the work, or will reject if the worker throws an unhandled error.
*/
class WorkerPool {
constructor(numWorkers, workerSource) {
this.idleWorkers = []; // Workers that are not currently working
this.workQueue = []; // Work not currently being processed
this.workerMap = new Map(); // Map workers to resolve and reject funcs
// Create the specified number of workers, add message and error
// handlers and save them in the idleWorkers array.
for (let i = 0; i < numWorkers; i++) {
let worker = new Worker(workerSource);
worker.onmessage = (message) => {
this._workerDone(worker, null, message.data);
};
worker.onerror = (error) => {
this._workerDone(worker, error, null);
};
this.idleWorkers[i] = worker;
}
}
// This internal method is called when a worker finishes working, either
// by sending a message or by throwing an error.
_workerDone(worker, error, response) {
// Look up the resolve() and reject() functions for this worker
// and then remove the worker's entry from the map.
let [resolver, rejector] = this.workerMap.get(worker);
this.workerMap.delete(worker);
// If there is no queued work, put this worker back in
// the list of idle workers. Otherwise, take work from the queue
// and send it to this worker.
if (this.workQueue.length === 0) {
this.idleWorkers.push(worker);
} else {
let [work, resolver, rejector] = this.workQueue.shift();
this.workerMap.set(worker, [resolver, rejector]);
worker.postMessage(work);
}
// Finally, resolve or reject the promise associated with the worker.
error === null ? resolver(response) : rejector(error);
}
// This method adds work to the worker pool and returns a Promise that
// will resolve with a worker's response when the work is done. The work
// is a value to be passed to a worker with postMessage(). If there is an
// idle worker, the work message will be sent immediately. Otherwise it
// will be queued until a worker is available.
addWork(work) {
return new Promise((resolve, reject) => {
if (this.idleWorkers.length > 0) {
let worker = this.idleWorkers.pop();
this.workerMap.set(worker, [resolve, reject]);
worker.postMessage(work);
} else {
this.workQueue.push([work, resolve, reject]);
}
});
}
}
/*
* This class holds the state information necessary to render a Mandelbrot set.
* The cx and cy properties give the point in the complex plane that is the
* center of the image. The perPixel property specifies how much the real and
* imaginary parts of that complex number changes for each pixel of the image.
* The maxIterations property specifies how hard we work to compute the set.
* Larger numbers require more computation but produce crisper images.
* Note that the size of the canvas is not part of the state. Given cx, cy, and
* perPixel we simply render whatever portion of the Mandelbrot set fits in
* the canvas at its current size.
*
* Objects of this type are used with history.pushState() and are used to read
* the desired state from a bookmarked or shared URL.
*/
class PageState {
// This factory method returns an initial state to display the entire set.
static initialState() {
let s = new PageState();
s.cx = -0.5;
s.cy = 0;
s.perPixel = 3 / window.innerHeight;
s.maxIterations = 500;
return s;
}
// This factory method obtains state from a URL, or returns null if
// a valid state could not be read from the URL.
static fromURL(url) {
let s = new PageState();
let u = new URL(url); // Initialize state from the url's search params.
s.cx = parseFloat(u.searchParams.get("cx"));
s.cy = parseFloat(u.searchParams.get("cy"));
s.perPixel = parseFloat(u.searchParams.get("pp"));
s.maxIterations = parseInt(u.searchParams.get("it"));
// If we got valid values, return the PageState object, otherwise null.
return isNaN(s.cx) ||
isNaN(s.cy) ||
isNaN(s.perPixel) ||
isNaN(s.maxIterations)
? null
: s;
}
// This instance method encodes the current state into the search
// parameters of the browser's current location.
toURL() {
let u = new URL(window.location);
u.searchParams.set("cx", this.cx);
u.searchParams.set("cy", this.cy);
u.searchParams.set("pp", this.perPixel);
u.searchParams.set("it", this.maxIterations);
return u.href;
}
}
// These constants control the parallelism of the Mandelbrot set computation.
// You may need to adjust them to get optimum performance on your computer.
const ROWS = 3,
COLS = 4,
NUMWORKERS = navigator.hardwareConcurrency || 2;
// This is the main class of our Mandelbrot set program. Simply invoke the
// constructor function with the <canvas> element to render into. The program
// assumes that this <canvas> element is styled so that it is always as big
// as the browser window.
class MandelbrotCanvas {
constructor(canvas) {
// Store the canvas, get its context object, and initialize a WorkerPool
this.canvas = canvas;
this.context = canvas.getContext("2d");
this.workerPool = new WorkerPool(NUMWORKERS, "mandelbrotWorker.js");
// Define some properties that we'll use later
this.tiles = null; // Subregions of the canvas
this.pendingRender = null; // We're not currently rendering
this.wantsRerender = false; // No render is currently requested
this.resizeTimer = null; // Prevents us from resizing too frequently
this.colorTable = null; // For converting raw data to pixel values.
// Set up our event handlers
this.canvas.addEventListener("pointerdown", (e) => this.handlePointer(e));
window.addEventListener("keydown", (e) => this.handleKey(e));
window.addEventListener("resize", (e) => this.handleResize(e));
window.addEventListener("popstate", (e) => this.setState(e.state, false));
// Initialize our state from the URL or start with the initial state.
this.state = PageState.fromURL(window.location) || PageState.initialState();
// Save this state with the history mechanism.
history.replaceState(this.state, "", this.state.toURL());
// Set the canvas size and get an array of tiles that cover it.
this.setSize();
// And render the Mandelbrot set into the canvas.
this.render();
}
// Set the canvas size and initialize an array of Tile objects. This
// method is called from the constructor and also by the handleResize()
// method when the browser window is resized.
setSize() {
this.width = this.canvas.width = window.innerWidth;
this.height = this.canvas.height = window.innerHeight;
this.tiles = [...Tile.tiles(this.width, this.height, ROWS, COLS)];
}
// This function makes a change to the PageState, then re-renders the
// Mandelbrot set using that new state, and also saves the new state with
// history.pushState(). If the first argument is a function that function
// will be called with the state object as its argument and should make
// changes to the state. If the first argument is an object, then we simply
// copy the properties of that object into the state object. If the optional
// second argument is false, then the new state will not be saved. (We
// do this when calling setState in response to a popstate event.)
setState(f, save = true) {
// If the argument is a function, call it to update the state.
// Otherwise, copy its properties into the current state.
if (typeof f === "function") {
f(this.state);
} else {
for (let property in f) {
this.state[property] = f[property];
}
}
// In either case, start rendering the new state ASAP.
this.render();
// Normally we save the new state. Except when we're called with
// a second argument of false which we do when we get a popstate event.
if (save) {
history.pushState(this.state, "", this.state.toURL());
}
}
// This method asynchronously draws the portion of the Mandelbrot set
// specified by the PageState object into the canvas. It is called by
// the constructor, by setState() when the state changes, and by the
// resize event handler when the size of the canvas changes.
render() {
// Sometimes the user may use the keyboard or mouse to request renders
// more quickly than we can perform them. We don't want to submit all
// the renders to the worker pool. Instead if we're rendering, we'll
// just make a note that a new render is needed, and when the current
// render completes, we'll render the current state, possibly skipping
// multiple intermediate states.
if (this.pendingRender) {
// If we're already rendering,
this.wantsRerender = true; // make a note to rerender later
return; // and don't do anything more now.
}
// Get our state variables and compute the complex number for the
// upper left corner of the canvas.
let { cx, cy, perPixel, maxIterations } = this.state;
let x0 = cx - (perPixel * this.width) / 2;
let y0 = cy - (perPixel * this.height) / 2;
// For each of our ROWS*COLS tiles, call addWork() with a message
// for the code in mandelbrotWorker.js. Collect the resulting Promise
// objects into an array.
let promises = this.tiles.map((tile) =>
this.workerPool.addWork({
tile: tile,
x0: x0 + tile.x * perPixel,
y0: y0 + tile.y * perPixel,
perPixel: perPixel,
maxIterations: maxIterations,
})
);
// Use Promise.all() to get an array of responses from the array of
// promises. Each response is the computation for one of our tiles.
// Recall from mandelbrotWorker.js that each response includes the
// Tile object, an ImageData object that includes iteration counts
// instead of pixel values, and the minimum and maximum iterations
// for that tile.
this.pendingRender = Promise.all(promises)
.then((responses) => {
// First, find the overall max and min iterations over all tiles.
// We need these numbers so we can assign colors to the pixels.
let min = maxIterations,
max = 0;
for (let r of responses) {
if (r.min < min) min = r.min;
if (r.max > max) max = r.max;
}
// Now we need a way to convert the raw iteration counts from the
// workers into pixel colors that will be displayed in the canvas.
// We know that all the pixels have between min and max iterations
// so we precompute the colors for each iteration count and store
// them in the colorTable array.
// If we haven't allocated a color table yet, or if it is no longer
// the right size, then allocate a new one.
if (!this.colorTable || this.colorTable.length !== maxIterations + 1) {
this.colorTable = new Uint32Array(maxIterations + 1);
}
// Given the max and the min, compute appropriate values in the
// color table. Pixels in the set will be colored fully opaque
// black. Pixels outside the set will be translucent black with higher
// iteration counts resulting in higher opacity. Pixels with
// minimum iteration counts will be transparent and the white
// background will show through, resulting in a grayscale image.
if (min === max) {
// If all the pixels are the same,
if (min === maxIterations) {
// Then make them all black
this.colorTable[min] = 0xff000000;
} else {
// Or all transparent.
this.colorTable[min] = 0;
}
} else {
// In the normal case where min and max are different, use a
// logarithic scale to assign each possible iteration count an
// opacity between 0 and 255, and then use the shift left
// operator to turn that into a pixel value.
let maxlog = Math.log(1 + max - min);
for (let i = min; i <= max; i++) {
this.colorTable[i] =
Math.ceil((Math.log(1 + i - min) / maxlog) * 255) << 24;
}
}
// Now translate the iteration numbers in each response's
// ImageData to colors from the colorTable.
for (let r of responses) {
let iterations = new Uint32Array(r.imageData.data.buffer);
for (let i = 0; i < iterations.length; i++) {
iterations[i] = this.colorTable[iterations[i]];
}
}
// Finally, render all the imageData objects into their
// corresponding tiles of the canvas using putImageData().
// (First, though, remove any CSS transforms on the canvas that may
// have been set by the pointerdown event handler.)
this.canvas.style.transform = "";
for (let r of responses) {
this.context.putImageData(r.imageData, r.tile.x, r.tile.y);
}
})
.catch((reason) => {
// If anything went wrong in any of our Promises, we'll log
// an error here. This shouldn't happen, but this will help with
// debugging if it does.
console.error("Promise rejected in render():", reason);
})
.finally(() => {
// When we are done rendering, clear the pendingRender flags
this.pendingRender = null;
// And if render requests came in while we were busy, rerender now.
if (this.wantsRerender) {
this.wantsRerender = false;
this.render();
}
});
}
// If the user resizes the window, this function will be called repeatedly.
// Resizing a canvas and rerendering the Mandlebrot set is an expensive
// operation that we can't do multiple times a second, so we use a timer
// to defer handling the resize until 200ms have elapsed since the last
// resize event was received.
handleResize(event) {
// If we were already deferring a resize, clear it.
if (this.resizeTimer) clearTimeout(this.resizeTimer);
// And defer this resize instead.
this.resizeTimer = setTimeout(() => {
this.resizeTimer = null; // Note that resize has been handled
this.setSize(); // Resize canvas and tiles
this.render(); // Rerender at the new size
}, 200);
}
// If the user presses a key, this event handler will be called.
// We call setState() in response to various keys, and setState() renders
// the new state, updates the URL, and saves the state in browser history.
handleKey(event) {
switch (event.key) {
case "Escape": // Type Escape to go back to the initial state
this.setState(PageState.initialState());
break;
case "+": // Type + to increase the number of iterations
this.setState((s) => {
s.maxIterations = Math.round(s.maxIterations * 1.5);
});
break;
case "-": // Type - to decrease the number of iterations
this.setState((s) => {
s.maxIterations = Math.round(s.maxIterations / 1.5);
if (s.maxIterations < 1) s.maxIterations = 1;
});
break;
case "o": // Type o to zoom out
this.setState((s) => (s.perPixel *= 2));
break;
case "ArrowUp": // Up arrow to scroll up
this.setState((s) => (s.cy -= (this.height / 10) * s.perPixel));
break;
case "ArrowDown": // Down arrow to scroll down
this.setState((s) => (s.cy += (this.height / 10) * s.perPixel));
break;
case "ArrowLeft": // Left arrow to scroll left
this.setState((s) => (s.cx -= (this.width / 10) * s.perPixel));
break;
case "ArrowRight": // Right arrow to scroll right
this.setState((s) => (s.cx += (this.width / 10) * s.perPixel));
break;
}
}
// This method is called when we get a pointerdown event on the canvas.
// The pointerdown event might be the start of a zoom gesture (a click or
// tap) or a pan gesture (a drag). This handler registers handlers for
// the pointermove and pointerup events in order to respond to the rest
// of the gesture. (These two extra handlers are removed when the gesture
// ends with a pointerup.)
handlePointer(event) {
// The pixel coordinates and time of the initial pointer down.
// Because the canvas is as big as the window, these event coordinates
// are also canvas coordinates.
const x0 = event.clientX,
y0 = event.clientY,
t0 = Date.now();
// This is the handler for move events.
const pointerMoveHandler = (event) => {
// How much have we moved, and how much time has passed?
let dx = event.clientX - x0,
dy = event.clientY - y0,
dt = Date.now() - t0;
// If the pointer has moved enough or enough time has passed that
// this is not a regular click, then use CSS to pan the display.
// (We will rerender it for real when we get the pointerup event.)
if (dx > 10 || dy > 10 || dt > 500) {
this.canvas.style.transform = `translate(${dx}px, ${dy}px)`;
}
};
// This is the handler for pointerup events
const pointerUpHandler = (event) => {
// When the pointer goes up, the gesture is over, so remove
// the move and up handlers until the next gesture.
this.canvas.removeEventListener("pointermove", pointerMoveHandler);
this.canvas.removeEventListener("pointerup", pointerUpHandler);
// How much did the pointer move, and how much time passed?
const dx = event.clientX - x0,
dy = event.clientY - y0,
dt = Date.now() - t0;
// Unpack the state object into individual constants.
const { cx, cy, perPixel } = this.state;
// If the pointer moved far enough or if enough time passed, then
// this was a pan gesture, and we need to change state to change
// the center point. Otherwise, the user clicked or tapped on a
// point and we need to center and zoom in on that point.
if (dx > 10 || dy > 10 || dt > 500) {
// The user panned the image by (dx, dy) pixels.
// Convert those values to offsets in the complex plane.
this.setState({ cx: cx - dx * perPixel, cy: cy - dy * perPixel });
} else {
// The user clicked. Compute how many pixels the center moves.
let cdx = x0 - this.width / 2;
let cdy = y0 - this.height / 2;
// Use CSS to quickly and temporarily zoom in
this.canvas.style.transform = `translate(${-cdx * 2}px, ${
-cdy * 2
}px) scale(2)`;
// Set the complex coordinates of the new center point and
// zoom in by a factor of 2.
this.setState((s) => {
s.cx += cdx * s.perPixel;
s.cy += cdy * s.perPixel;
s.perPixel /= 2;
});
}
};
// When the user begins a gesture we register handlers for the
// pointermove and pointerup events that follow.
this.canvas.addEventListener("pointermove", pointerMoveHandler);
this.canvas.addEventListener("pointerup", pointerUpHandler);
}
}
// Finally, here's how we set up the canvas. Note that this JavaScript file
// is self-sufficient. The HTML file only needs to include this one <script>.
let canvas = document.createElement("canvas"); // Create a canvas element
document.body.append(canvas); // Insert it into the body
document.body.style = "margin:0"; // No margin for the <body>
canvas.style.width = "100%"; // Make canvas as wide as body
canvas.style.height = "100%"; // and as high as the body.
new MandelbrotCanvas(canvas); // And start rendering into it!
// This is a simple worker that receives a message from its parent thread,
// performs the computation described by that message and then posts the
// result of that computation back to the parent thread.
onmessage = function (message) {
// First, we unpack the message we received:
// - tile is an object with width and height properties. It specifies the
// size of the rectangle of pixels for which we will be computing
// Mandelbrot set membership.
// - (x0, y0) is the point in the complex plane that corresponds to the
// upper-left pixel in the tile.
// - perPixel is the pixel size in both the real and imaginary dimensions.
// - maxIterations specifies the maximum number of iterations we will
// perform before deciding that a pixel is in the set.
const { tile, x0, y0, perPixel, maxIterations } = message.data;
const { width, height } = tile;
// Next, we create an ImageData object to represent the rectangular array
// of pixels, get its internal ArrayBuffer, and create a typed array view
// of that buffer so we can treat each pixel as a single integer instead of
// four individual bytes. We'll store the number of iterations for each
// pixel in this iterations array. (The iterations will be transformed into
// actual pixel colors in the parent thread.)
const imageData = new ImageData(width, height);
const iterations = new Uint32Array(imageData.data.buffer);
// Now we begin the computation. There are three nested for loops here.
// The outer two loop over the rows and columns of pixels, and the inner
// loop iterates each pixel to see if it "escapes" or not. The various
// loop variables are the following:
// - row and column are integers representing the pixel coordinate.
// - x and y represent the complex point for each pixel: x + yi.
// - index is the index in the iterations array for the current pixel.
// - n tracks the number of iterations for each pixel.
// - max and min track the largest and smallest number of iterations
// we've seen so far for any pixel in the rectangle.
let index = 0,
max = 0,
min = maxIterations;
for (let row = 0, y = y0; row < height; row++, y += perPixel) {
for (let column = 0, x = x0; column < width; column++, x += perPixel) {
// For each pixel we start with the complex number c = x+yi.
// Then we repeatedly compute the complex number z(n+1) based on
// this recursive formula:
// z(0) = c
// z(n+1) = z(n)^2 + c
// If |z(n)| (the magnitude of z(n)) is > 2, then the
// pixel is not part of the set and we stop after n iterations.
let n; // The number of iterations so far
let r = x,
i = y; // Start with z(0) set to c
for (n = 0; n < maxIterations; n++) {
let rr = r * r,
ii = i * i; // Square the two parts of z(n).
if (rr + ii > 4) {
// If |z(n)|^2 is > 4 then
break; // we've escaped and can stop iterating.
}
i = 2 * r * i + y; // Compute imaginary part of z(n+1).
r = rr - ii + x; // And the real part of z(n+1).
}
iterations[index++] = n; // Remember # iterations for each pixel.
if (n > max) max = n; // Track the maximum number we've seen.
if (n < min) min = n; // And the minimum as well.
}
}
// When the computation is complete, send the results back to the parent
// thread. The imageData object will be copied, but the giant ArrayBuffer
// it contains will be transferred for a nice performance boost.
postMessage({ tile, imageData, min, max }, [imageData.data.buffer]);
};
// Return an iterable object that iterates the result of applying f()
// to each value from the source iterable
function map(iterable, f) {
let iterator = iterable[Symbol.iterator]();
return {
// This object is both iterator and iterable
[Symbol.iterator]() {
return this;
},
next() {
let v = iterator.next();
if (v.done) {
return v;
} else {
return { value: f(v.value) };
}
},
};
}
// Map a range of integers to their squares and convert to an array
[...map(new Range(1, 4), (x) => x * x)]; // => [1, 4, 9, 16]
// Return a function that expects an array argument and applies f to
// each element, returning the array of return values.
// Contrast this with the map() function from earlier.
function mapper(f) {
return (a) => map(a, f);
}
const increment = (x) => x + 1;
const incrementAll = mapper(increment);
incrementAll([1, 2, 3]); // => [2,3,4]
// Return a memoized version of f.
// It only works if arguments to f all have distinct string representations.
function memoize(f) {
const cache = new Map(); // Value cache stored in the closure.
return function (...args) {
// Create a string version of the arguments to use as a cache key.
let key = args.length + args.join("+");
if (cache.has(key)) {
return cache.get(key);
} else {
let result = f.apply(this, args);
cache.set(key, result);
return result;
}
};
}
// Return the Greatest Common Divisor of two integers using the Euclidian
// algorithm: http://en.wikipedia.org/wiki/Euclidean_algorithm
function gcd(a, b) {
// Type checking for a and b has been omitted
if (a < b) {
// Ensure that a >= b when we start
[a, b] = [b, a]; // Destructuring assignment to swap variables
}
while (b !== 0) {
// This is Euclid's algorithm for GCD
[a, b] = [b, a % b];
}
return a;
}
const gcdmemo = memoize(gcd);
gcdmemo(85, 187); // => 17
// Note that when we write a recursive function that we will be memoizing,
// we typically want to recurse to the memoized version, not the original.
const factorial = memoize(function (n) {
return n <= 1 ? 1 : n * factorial(n - 1);
});
factorial(5); // => 120: also caches values for 4, 3, 2 and 1.
// Like Object.assign() but doesn't override existing properties
// (and also doesn't handle Symbol properties)
function merge(target, ...sources) {
for (let source of sources) {
for (let key of Object.keys(source)) {
if (!(key in target)) {
// This is different than Object.assign()
target[key] = source[key];
}
}
}
return target;
}
Object.assign({ x: 1 }, { x: 2, y: 2 }, { y: 3, z: 4 }); // => {x: 2, y: 3, z: 4}
merge({ x: 1 }, { x: 2, y: 2 }, { y: 3, z: 4 }); // => {x: 1, y: 2, z: 4}
// This higher-order function returns a new function that passes its
// arguments to f and returns the logical negation of f's return value;
function not(f) {
return function (...args) {
// Return a new function
let result = f.apply(this, args); // that calls f
return !result; // and negates its result.
};
}
const even = (x) => x % 2 === 0; // A function to determine if a number is even
const odd = not(even); // A new function that does the opposite
[1, 1, 3, 5, 5].every(odd); // => true: every element of the array is odd
// Set the onload property of the Window object to a function.
// The function is the event handler: it is invoked when the document loads.
window.onload = function () {
// Look up a <form> element
let form = document.querySelector("form#shipping");
// Register an event handler function on the form that will be invoked
// before the form is submitted. Assume isFormValid() is defined elsewhere.
form.onsubmit = function (event) {
// When the user submits the form
if (!isFormValid(this)) {
// check whether form inputs are valid
event.preventDefault(); // and if not, prevent form submission.
}
};
};
// We define some simple functions here
function add(x, y) {
return x + y;
}
function subtract(x, y) {
return x - y;
}
function multiply(x, y) {
return x * y;
}
function divide(x, y) {
return x / y;
}
// Here's a function that takes one of the preceding functions
// as an argument and invokes it on two operands
function operate(operator, operand1, operand2) {
return operator(operand1, operand2);
}
// We could invoke this function like this to compute the value (2+3) + (4*5):
let i = operate(add, operate(add, 2, 3), operate(multiply, 4, 5));
// For the sake of the example, we implement the simple functions again,
// this time within an object literal;
const operators = {
add: (x, y) => x + y,
subtract: (x, y) => x - y,
multiply: (x, y) => x * y,
divide: (x, y) => x / y,
pow: Math.pow, // This works for predefined functions too
};
// This function takes the name of an operator, looks up that operator
// in the object, and then invokes it on the supplied operands. Note
// the syntax used to invoke the operator function.
function operate2(operation, operand1, operand2) {
if (typeof operators[operation] === "function") {
return operators[operation](operand1, operand2);
} else throw "unknown operator";
}
operate2("add", "hello", operate2("add", " ", "world")); // => "hello world"
operate2("pow", 10, 2); // => 100
const child_process = require("child_process");
const util = require("util");
const execP = util.promisify(child_process.exec);
function parallelExec(commands) {
// Use the array of commands to create an array of Promises
let promises = commands.map((command) =>
execP(command, { encoding: "utf8" })
);
// Return a Promise that will fulfill to an array of the fulfillment
// values of each of the individual promises. (Instead of returning objects
// with stdout and stderr properties we just return the stdout value.)
return Promise.all(promises).then((outputs) =>
outputs.map((out) => out.stdout)
);
}
module.exports = parallelExec;
const threads = require("worker_threads");
if (threads.isMainThread) {
// In the main thread, we create a shared typed array with
// one element. Both threads will be able to read and write
// sharedArray[0] at the same time.
let sharedBuffer = new SharedArrayBuffer(4);
let sharedArray = new Int32Array(sharedBuffer);
// Now create a worker thread, passing the shared array to it with
// as its initial workerData value so we don't have to bother with
// sending and receiving a message
let worker = new threads.Worker(__filename, { workerData: sharedArray });
// Wait for the worker to start running and then increment the
// shared integer 10 million times.
worker.on("online", () => {
for (let i = 0; i < 10_000_000; i++) sharedArray[0]++;
// Once we're done with our increments, we start listening for
// message events so we know when the worker is done.
worker.on("message", () => {
// Although the shared integer has been incremented
// 20 million times, its value will generally be much less.
// On my computer the final value is typically under 12 million.
console.log(sharedArray[0]);
});
});
} else {
// In the worker thread, we get the shared array from workerData
// and then increment it 10 million times.
let sharedArray = threads.workerData;
for (let i = 0; i < 10_000_000; i++) sharedArray[0]++;
// When we're done incrementing, let the main thread know
threads.parentPort.postMessage("done");
}
const child_process = require("child_process");
// Start a new node process running the code in child.js in our directory
let child = child_process.fork(`${__dirname}/child.js`);
// Send a message to the child
child.send({ x: 4, y: 3 });
// Print the child's response when it arrives.
child.on("message", (message) => {
console.log(message.hypotenuse); // This should print "5"
// Since we only send one message we only expect one response.
// After we receive it we call disconnect() to terminate the connection
// between parent and child. This allows both processes to exit cleanly.
child.disconnect();
});
// The arguments to this function are passed on the left
function partialLeft(f, ...outerArgs) {
return function (...innerArgs) {
// Return this function
let args = [...outerArgs, ...innerArgs]; // Build the argument list
return f.apply(this, args); // Then invoke f with it
};
}
// The arguments to this function are passed on the right
function partialRight(f, ...outerArgs) {
return function (...innerArgs) {
// Return this function
let args = [...innerArgs, ...outerArgs]; // Build the argument list
return f.apply(this, args); // Then invoke f with it
};
}
// The arguments to this function serve as a template. Undefined values
// in the argument list are filled in with values from the inner set.
function partial(f, ...outerArgs) {
return function (...innerArgs) {
let args = [...outerArgs]; // local copy of outer args template
let innerIndex = 0; // which inner arg is next
// Loop through the args, filling in undefined values from inner args
for (let i = 0; i < args.length; i++) {
if (args[i] === undefined) args[i] = innerArgs[innerIndex++];
}
// Now append any remaining inner arguments
args.push(...innerArgs.slice(innerIndex));
return f.apply(this, args);
};
}
// Here is a function with three arguments
const f = function (x, y, z) {
return x * (y - z);
};
// Notice how these three partial applications differ
partialLeft(f, 2)(3, 4); // => -2: Bind first argument: 2 * (3 - 4)
partialRight(f, 2)(3, 4); // => 6: Bind last argument: 3 * (4 - 2)
partial(f, undefined, 2)(3, 4); // => -6: Bind middle argument: 3 * (2 - 4)
const increment = partialLeft(sum, 1);
const cuberoot = partialRight(Math.pow, 1 / 3);
cuberoot(increment(26)); // => 3
const not = partialLeft(compose, (x) => !x);
const even = (x) => x % 2 === 0;
const odd = not(even);
const isNumber = not(isNaN);
odd(3) && isNumber(2); // => true
// sum() and square() functions are defined above. Here are some more:
const product = (x, y) => x * y;
const neg = partial(product, -1);
const sqrt = partial(Math.pow, undefined, 0.5);
const reciprocal = partial(Math.pow, undefined, neg(1));
// Now compute the mean and standard deviation.
let data = [1, 1, 3, 5, 5]; // Our data
let mean = product(reduce(data, sum), reciprocal(data.length));
let stddev = sqrt(
product(
reduce(map(data, compose(square, partial(sum, neg(mean)))), sum),
reciprocal(sum(data.length, neg(1)))
)
);
[mean, stddev]; // => [3, 2]
/**
* Create an <svg> element and draw a pie chart into it.
*
* This function expects an object argument with the following properties:
*
* width, height: the size of the SVG graphic, in pixels
* cx, cy, r: the center and radius of the pie
* lx, ly: the upper-left corner of the chart legend
* data: an object whose property names are data labels and whose
* property values are the values associated with each label
*
* The function returns an <svg> element. The caller must insert it into
* the document in order to make it visible.
*/
function pieChart(options) {
let { width, height, cx, cy, r, lx, ly, data } = options;
// This is the XML namespace for svg elements
let svg = "http://www.w3.org/2000/svg";
// Create the <svg> element, and specify pixel size and user coordinates
let chart = document.createElementNS(svg, "svg");
chart.setAttribute("width", width);
chart.setAttribute("height", height);
chart.setAttribute("viewBox", `0 0 ${width} ${height}`);
// Define the text styles we'll use for the chart. If we leave these
// values unset here, they can be set with CSS instead.
chart.setAttribute("font-family", "sans-serif");
chart.setAttribute("font-size", "18");
// Get labels and values as arrays and add up the values so we know how
// big the pie is.
let labels = Object.keys(data);
let values = Object.values(data);
let total = values.reduce((x, y) => x + y);
// Figure out the angles for all the slices. Slice i starts at angles[i]
// and ends at angles[i+1]. The angles are measured in radians.
let angles = [0];
values.forEach((x, i) => angles.push(angles[i] + (x / total) * 2 * Math.PI));
// Now loop through the slices of the pie
values.forEach((value, i) => {
// Compute the two points where our slice intersects the circle
// These formulas are chosen so that an angle of 0 is at 12 o'clock
// and positive angles increase clockwise.
let x1 = cx + r * Math.sin(angles[i]);
let y1 = cy - r * Math.cos(angles[i]);
let x2 = cx + r * Math.sin(angles[i + 1]);
let y2 = cy - r * Math.cos(angles[i + 1]);
// This is a flag for angles larger than a half circle
// It is required by the SVG arc drawing component
let big = angles[i + 1] - angles[i] > Math.PI ? 1 : 0;
// This string describes how to draw a slice of the pie chart:
let path =
`M${cx},${cy}` + // Move to circle center.
`L${x1},${y1}` + // Draw line to (x1,y1).
`A${r},${r} 0 ${big} 1` + // Draw an arc of radius r...
`${x2},${y2}` + // ...ending at to (x2,y2).
"Z"; // Close path back to (cx,cy).
// Compute the CSS color for this slice. This formula works for only
// about 15 colors. So don't include more than 15 slices in a chart.
let color = `hsl(${(i * 40) % 360},${90 - 3 * i}%,${50 + 2 * i}%)`;
// We describe a slice with a <path> element. Note createElementNS().
let slice = document.createElementNS(svg, "path");
// Now set attributes on the <path> element
slice.setAttribute("d", path); // Set the path for this slice
slice.setAttribute("fill", color); // Set slice color
slice.setAttribute("stroke", "black"); // Outline slice in black
slice.setAttribute("stroke-width", "1"); // 1 CSS pixel thick
chart.append(slice); // Add slice to chart
// Now draw a little matching square for the key
let icon = document.createElementNS(svg, "rect");
icon.setAttribute("x", lx); // Position the square
icon.setAttribute("y", ly + 30 * i);
icon.setAttribute("width", 20); // Size the square
icon.setAttribute("height", 20);
icon.setAttribute("fill", color); // Same fill color as slice
icon.setAttribute("stroke", "black"); // Same outline, too.
icon.setAttribute("stroke-width", "1");
chart.append(icon); // Add to the chart
// And add a label to the right of the rectangle
let label = document.createElementNS(svg, "text");
label.setAttribute("x", lx + 30); // Position the text
label.setAttribute("y", ly + 30 * i + 16);
label.append(`${labels[i]} ${value}`); // Add text to label
chart.append(label); // Add label to the chart
});
return chart;
}
function pipe(readable, writable, callback) {
// First, set up error handling
function handleError(err) {
readable.close();
writable.close();
callback(err);
}
// Next define the pipe and handle the normal termination case
readable
.on("error", handleError)
.pipe(writable)
.on("error", handleError)
.on("finish", callback);
}
// Define a regular polygon with n sides, centered at (x,y) with radius r.
// The vertices are equally spaced along the circumference of a circle.
// Put the first vertex straight up or at the specified angle.
// Rotate clockwise, unless the last argument is true.
function polygon(c, n, x, y, r, angle = 0, counterclockwise = false) {
c.moveTo(
x + r * Math.sin(angle), // Begin a new subpath at the first vertex
y - r * Math.cos(angle)
); // Use trigonometry to compute position
let delta = (2 * Math.PI) / n; // Angular distance between vertices
for (let i = 1; i < n; i++) {
// For each of the remaining vertices
angle += counterclockwise ? -delta : delta; // Adjust angle
c.lineTo(
x + r * Math.sin(angle), // Add line to next vertex
y - r * Math.cos(angle)
);
}
c.closePath(); // Connect last vertex back to the first
}
// Assume there is just one canvas, and get its context object to draw with.
let c = document.querySelector("canvas").getContext("2d");
// Start a new path and add polygon subpaths
c.beginPath();
polygon(c, 3, 50, 70, 50); // Triangle
polygon(c, 4, 150, 60, 50, Math.PI / 4); // Square
polygon(c, 5, 255, 55, 50); // Pentagon
polygon(c, 6, 365, 53, 50, Math.PI / 6); // Hexagon
polygon(c, 4, 365, 53, 20, Math.PI / 4, true); // Small square inside the hexagon
// Set some properties that control how the graphics will look
c.fillStyle = "#ccc"; // Light gray interiors
c.strokeStyle = "#008"; // outlined with dark blue lines
c.lineWidth = 5; // five pixels wide.
// Now draw all the polygons (each in its own subpath) with these calls
c.fill(); // Fill the shapes
c.stroke(); // And stroke their outlines
const https = require("https");
/*
* Convert the body object to a JSON string then HTTPS POST it to the
* specified API endpoint on the specified host. When the response arrives,
* parse the response body as JSON and resolve the returned Promise with
* that parsed value.
*/
function postJSON(host, endpoint, body, port, username, password) {
// Return a Promise object immediately, then call resolve or reject
// when the HTTPS request succeeds or fails.
return new Promise((resolve, reject) => {
// Convert the body object to a string
let bodyText = JSON.stringify(body);
// Configure the HTTPS request
let requestOptions = {
method: "POST", // Or "GET", "PUT", "DELETE", etc.
host: host, // The host to connect to
path: endpoint, // The URL path
headers: {
// HTTP headers for the request
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(bodyText),
},
};
if (port) {
// If a port is specified,
requestOptions.port = port; // use it for the request.
}
// If credentials are specified, add an Authorization header.
if (username && password) {
requestOptions.auth = `${username}:${password}`;
}
// Now create the request based on the configuration object
let request = https.request(requestOptions);
// Write the body of the POST request and end the request.
request.write(bodyText);
request.end();
// Fail on request errors (such as no network connection)
request.on("error", (e) => reject(e));
// Handle the response when it starts to arrive.
request.on("response", (response) => {
if (response.statusCode !== 200) {
reject(new Error(`HTTP status ${response.statusCode}`));
// We don't care about the response body in this case, but
// we don't want it to stick around in a buffer somewhere, so
// we put the stream into flowing mode without registering
// a "data" handler so that the body is discarded.
response.resume();
return;
}
// We want text, not bytes. We're assuming the text will be
// JSON-formatted but aren't bothering to check the
// Content-Type header.
response.setEncoding("utf8");
// Node doesn't have a streaming JSON parser, so we read the
// entire response body into a string.
let body = "";
response.on("data", (chunk) => {
body += chunk;
});
// And now handle the response when it is complete.
response.on("end", () => {
// When the response is done,
try {
// try to parse it as JSON
resolve(JSON.parse(body)); // and resolve the result.
} catch (e) {
// Or, if anything goes wrong,
reject(e); // reject with the error
}
});
});
});
}
// This function takes an array of input values and a "promiseMaker" function.
// For any input value x in the array, promiseMaker(x) should return a Promise
// that will fulfill to an output value. This function returns a Promise
// that fulfills to an array of the computed output values.
//
// Rather than creating the Promises all at once and letting them run in
// parallel, however, promiseSequence() only runs one Promise at a time
// and does not call promiseMaker() for a value until the previous Promise
// has fulfilled.
function promiseSequence(inputs, promiseMaker) {
// Make a private copy of the array that we can modify
inputs = [...inputs];
// Here's the function that we'll use as a Promise callback
// This is the pseudorecursive magic that makes this all work.
function handleNextInput(outputs) {
if (inputs.length === 0) {
// If there are no more inputs left, then return the array
// of outputs, finally fulfilling this Promise and all the
// previous resolved-but-not-fulfilled Promises.
return outputs;
} else {
// If there are still input values to process, then we'll
// return a Promise object, resolving the current Promise
// with the future value from a new Promise.
let nextInput = inputs.shift(); // Get the next input value,
return (
promiseMaker(nextInput) // compute the next output value,
// Then create a new outputs array with the new output value
.then((output) => outputs.concat(output))
// Then "recurse", passing the new, longer, outputs array
.then(handleNextInput)
);
}
}
// Start with a Promise that fulfills to an empty array and use
// the function above as its callback.
return Promise.resolve([]).then(handleNextInput);
}
// Given a URL, return a Promise that fulfills to the URL body text
function fetchBody(url) {
return fetch(url).then((r) => r.text());
}
// Use it to sequentially fetch a bunch of URL bodies
promiseSequence(urls, fetchBody)
.then((bodies) => {
/* do something with the array of strings */
})
.catch(console.error);
// This object has accessor properties that return random numbers.
// The expression "random.octet", for example, yields a random number
// between 0 and 255 each time it is evaluated.
const random = {
get octet() {
return Math.floor(Math.random() * 256);
},
get uint16() {
return Math.floor(Math.random() * 65536);
},
get int16() {
return Math.floor(Math.random() * 65536) - 32768;
},
};
/*
* A Range object represents a range of numbers {x: from <= x <= to}
* Range defines a has() method for testing whether a given number is a member
* of the range. Range is iterable and iterates all integers within the range.
*/
class Range {
constructor(from, to) {
this.from = from;
this.to = to;
}
// Make a Range act like a Set of numbers
has(x) {
return typeof x === "number" && this.from <= x && x <= this.to;
}
// Return string representation of the range using set notation
toString() {
return `{ x | ${this.from} ≤ x ≤ ${this.to} }`;
}
// Make a Range iterable by returning an iterator object.
// Note that the name of this method is a special symbol, not a string.
[Symbol.iterator]() {
// Each iterator instance must iterate the range independently of
// others. So we need a state variable to track our location in the
// iteration. We start at the first integer >= from.
let next = Math.ceil(this.from); // This is the next value we return
let last = this.to; // We won't return anything > this
return {
// This is the iterator object
// This next() method is what makes this an iterator object.
// It must return an iterator result object.
next() {
return next <= last // If we haven't returned last value yet
? { value: next++ } // return next value and increment it
: { done: true }; // otherwise indicate that we're done.
},
// As a convenience, we make the iterator itself iterable.
[Symbol.iterator]() {
return this;
},
};
}
}
for (let x of new Range(1, 10)) console.log(x); // Logs numbers 1 to 10
[...new Range(-2, 2)]; // => [-2, -1, 0, 1, 2]
// This is a factory function that returns a new range object.
function range(from, to) {
// Use Object.create() to create an object that inherits from the
// prototype object defined below. The prototype object is stored as
// a property of this function, and defines the shared methods (behavior)
// for all range objects.
let r = Object.create(range.methods);
// Store the start and end points (state) of this new range object.
// These are noninherited properties that are unique to this object.
r.from = from;
r.to = to;
// Finally return the new object
return r;
}
// This prototype object defines methods inherited by all range objects.
range.methods = {
// Return true if x is in the range, false otherwise
// This method works for textual and Date ranges as well as numeric.
includes(x) {
return this.from <= x && x <= this.to;
},
// A generator function that makes instances of the class iterable.
// Note that it only works for numeric ranges.
*[Symbol.iterator]() {
for (let x = Math.ceil(this.from); x <= this.to; x++) yield x;
},
// Return a string representation of the range
toString() {
return "(" + this.from + "..." + this.to + ")";
},
};
// This is a constructor function that initializes new Range objects.
// Note that it does not create or return the object. It just initializes this.
function Range(from, to) {
// Store the start and end points (state) of this new range object.
// These are noninherited properties that are unique to this object.
this.from = from;
this.to = to;
}
// All Range objects inherit from this object.
// Note that the property name must be "prototype" for this to work.
Range.prototype = {
// Return true if x is in the range, false otherwise
// This method works for textual and Date ranges as well as numeric.
includes: function (x) {
return this.from <= x && x <= this.to;
},
// A generator function that makes instances of the class iterable.
// Note that it only works for numeric ranges.
[Symbol.iterator]: function* () {
for (let x = Math.ceil(this.from); x <= this.to; x++) yield x;
},
// Return a string representation of the range
toString: function () {
return "(" + this.from + "..." + this.to + ")";
},
};
class Range {
constructor(from, to) {
// Store the start and end points (state) of this new range object.
// These are noninherited properties that are unique to this object.
this.from = from;
this.to = to;
}
// Return true if x is in the range, false otherwise
// This method works for textual and Date ranges as well as numeric.
includes(x) {
return this.from <= x && x <= this.to;
}
// A generator function that makes instances of the class iterable.
// Note that it only works for numeric ranges.
*[Symbol.iterator]() {
for (let x = Math.ceil(this.from); x <= this.to; x++) yield x;
}
// Return a string representation of the range
toString() {
return `(${this.from}...${this.to})`;
}
}
const fs = require("fs"); // Require the filesystem module
// Read a config file, parse its contents as JSON, and pass the
// resulting value to the callback. If anything goes wrong,
// print an error message to stderr and invoke the callback with null
function readConfigFile(path, callback) {
fs.readFile(path, "utf8", (err, text) => {
if (err) {
// Something went wrong reading the file
console.error(err);
callback(null);
return;
}
let data = null;
try {
data = JSON.parse(text);
} catch (e) {
// Something went wrong parsing the file contents
console.error(e);
}
callback(data);
});
}
const util = require("util");
const fs = require("fs"); // Require the filesystem module
const pfs = {
// Promise-based variants of some fs functions
readFile: util.promisify(fs.readFile),
};
function readConfigFile(path) {
return pfs.readFile(path, "utf-8").then((text) => {
return JSON.parse(text);
});
}
const fs = require("fs");
const util = require("util");
const pfs = { readFile: util.promisify(fs.readFile) };
async function readConfigFile(path) {
let text = await pfs.readFile(path, "utf-8");
return JSON.parse(text);
}
const fs = require("fs");
function readConfigFileSync(path) {
let text = fs.readFileSync(path, "utf-8");
return JSON.parse(text);
}
function readOnlyProxy(o) {
function readonly() {
throw new TypeError("Readonly");
}
return new Proxy(o, {
set: readonly,
defineProperty: readonly,
deleteProperty: readonly,
setPrototypeOf: readonly,
});
}
let o = { x: 1, y: 2 }; // Normal writable object
let p = readOnlyProxy(o); // Readonly version of it
p.x; // => 1: reading properties works
p.x = 2; // !TypeError: can't change properties
delete p.y; // !TypeError: can't delete properties
p.z = 3; // !TypeError: can't add properties
p.__proto__ = {}; // !TypeError: can't change the prototype
/**
* This class defines a custom HTML <search-box> element that displays an
* <input> text input field plus two icons or emoji. By default, it displays a
* magnifying glass emoji (indicating search) to the left of the text field
* and an X emoji (indicating cancel) to the right of the text field. It
* hides the border on the input field and displays a border around itself,
* creating the appearance that the two emoji are inside the input
* field. Similarly, when the internal input field is focused, the focus ring
* is displayed around the <search-box>.
*
* You can override the default icons by including <span> or <img> children
* of <search-box> with slot="left" and slot="right" attributes.
*
* <search-box> supports the normal HTML disabled and hidden attributes and
* also size and placeholder attributes, which have the same meaning for this
* element as they do for the <input> element.
*
* Input events from the internal <input> element bubble up and appear with
* their target field set to the <search-box> element.
*
* The element fires a "search" event with the detail property set to the
* current input string when the user clicks on the left emoji (the magnifying
* glass). The "search" event is also dispatched when the internal text field
* generates a "change" event (when the text has changed and the user types
* Return or Tab).
*
* The element fires a "clear" event when the user clicks on the right emoji
* (the X). If no handler calls preventDefault() on the event then the element
* clears the user's input once event dispatch is complete.
*
* Note that there are no onsearch and onclear properties or attributes:
* handlers for the "search" and "clear" events can only be registered with
* addEventListener().
*/
class SearchBox extends HTMLElement {
constructor() {
super(); // Invoke the superclass constructor; must be first.
// Create a shadow DOM tree and attach it to this element, setting
// the value of this.shadowRoot.
this.attachShadow({ mode: "open" });
// Clone the template that defines the descendants and stylesheet for
// this custom component, and append that content to the shadow root.
this.shadowRoot.append(SearchBox.template.content.cloneNode(true));
// Get references to the important elements in the shadow DOM
this.input = this.shadowRoot.querySelector("#input");
let leftSlot = this.shadowRoot.querySelector('slot[name="left"]');
let rightSlot = this.shadowRoot.querySelector('slot[name="right"]');
// When the internal input field gets or loses focus, set or remove
// the "focused" attribute which will cause our internal stylesheet
// to display or hide a fake focus ring on the entire component. Note
// that the "blur" and "focus" events bubble and appear to originate
// from the <search-box>.
this.input.onfocus = () => {
this.setAttribute("focused", "");
};
this.input.onblur = () => {
this.removeAttribute("focused");
};
// If the user clicks on the magnifying glass, trigger a "search"
// event. Also trigger it if the input field fires a "change"
// event. (The "change" event does not bubble out of the Shadow DOM.)
leftSlot.onclick = this.input.onchange = (event) => {
event.stopPropagation(); // Prevent click events from bubbling
if (this.disabled) return; // Do nothing when disabled
this.dispatchEvent(
new CustomEvent("search", {
detail: this.input.value,
})
);
};
// If the user clicks on the X, trigger a "clear" event.
// If preventDefault() is not called on the event, clear the input.
rightSlot.onclick = (event) => {
event.stopPropagation(); // Don't let the click bubble up
if (this.disabled) return; // Don't do anything if disabled
let e = new CustomEvent("clear", { cancelable: true });
this.dispatchEvent(e);
if (!e.defaultPrevented) {
// If the event was not "cancelled"
this.input.value = ""; // then clear the input field
}
};
}
// When some of our attributes are set or changed, we need to set the
// corresponding value on the internal <input> element. This life cycle
// method, together with the static observedAttributes property below,
// takes care of that.
attributeChangedCallback(name, oldValue, newValue) {
if (name === "disabled") {
this.input.disabled = newValue !== null;
} else if (name === "placeholder") {
this.input.placeholder = newValue;
} else if (name === "size") {
this.input.size = newValue;
} else if (name === "value") {
this.input.value = newValue;
}
}
// Finally, we define property getters and setters for properties that
// correspond to the HTML attributes we support. The getters simply return
// the value (or the presence) of the attribute. And the setters just set
// the value (or the presence) of the attribute. When a setter method
// changes an attribute, the browser will automatically invoke the
// attributeChangedCallback above.
get placeholder() {
return this.getAttribute("placeholder");
}
get size() {
return this.getAttribute("size");
}
get value() {
return this.getAttribute("value");
}
get disabled() {
return this.hasAttribute("disabled");
}
get hidden() {
return this.hasAttribute("hidden");
}
set placeholder(value) {
this.setAttribute("placeholder", value);
}
set size(value) {
this.setAttribute("size", value);
}
set value(text) {
this.setAttribute("value", text);
}
set disabled(value) {
if (value) this.setAttribute("disabled", "");
else this.removeAttribute("disabled");
}
set hidden(value) {
if (value) this.setAttribute("hidden", "");
else this.removeAttribute("hidden");
}
}
// This static field is required for the attributeChangedCallback method.
// Only attributes named in this array will trigger calls to that method.
SearchBox.observedAttributes = ["disabled", "placeholder", "size", "value"];
// Create a <template> element to hold the stylesheet and the tree of
// elements that we'll use for each instance of the SearchBox element.
SearchBox.template = document.createElement("template");
// We initialize the template by parsing this string of HTML. Note, however,
// that when we instantiate a SearchBox, we are able to just clone the nodes
// in the template and do have to parse the HTML again.
SearchBox.template.innerHTML = `
<style>
/*
* The :host selector refers to the <search-box> element in the light
* DOM. These styles are defaults and can be overridden by the user of the
* <search-box> with styles in the light DOM.
*/
:host {
display: inline-block; /* The default is inline display */
border: solid black 1px; /* A rounded border around the <input> and <slots> */
border-radius: 5px;
padding: 4px 6px; /* And some space inside the border */
}
:host([hidden]) { /* Note the parentheses: when host has hidden... */
display:none; /* ...attribute set don't display it */
}
:host([disabled]) { /* When host has the disabled attribute... */
opacity: 0.5; /* ...gray it out */
}
:host([focused]) { /* When host has the focused attribute... */
box-shadow: 0 0 2px 2px #6AE; /* display this fake focus ring. */
}
/* The rest of the stylesheet only applies to elements in the Shadow DOM. */
input {
border-width: 0; /* Hide the border of the internal input field. */
outline: none; /* Hide the focus ring, too. */
font: inherit; /* <input> elements don't inherit font by default */
background: inherit; /* Same for background color. */
}
slot {
cursor: default; /* An arrow pointer cursor over the buttons */
user-select: none; /* Don't let the user select the emoji text */
}
</style>
<div>
<slot name="left">\u{1f50d}</slot> <!-- U+1F50D is a magnifying glass -->
<input type="text" id="input" /> <!-- The actual input element -->
<slot name="right">\u{2573}</slot> <!-- U+2573 is an X -->
</div>
`;
// Finally, we call customElement.define() to register the SearchBox element
// as the implementation of the <search-box> tag. Custom elements are required
// to have a tag name that contains a hyphen.
customElements.define("search-box", SearchBox);
function* sequence(...iterables) {
for (let iterable of iterables) {
yield* iterable;
}
}
[...sequence("abc", oneDigitPrimes())]; // => ["a","b","c",2,3,5,7]
// This object generates strictly increasing serial numbers
const serialnum = {
// This data property holds the next serial number.
// The _ in the property name hints that it is for internal use only.
_n: 0,
// Return the current value and increment it
get next() {
return this._n++;
},
// Set a new value of n, but only if it is larger than current
set next(n) {
if (n > this._n) this._n = n;
else throw new Error("serial number can only be set to a larger value");
},
};
serialnum.next = 10; // Set the starting serial number
serialnum.next; // => 10
serialnum.next; // => 11: different value each time we get next
// Store the name/value pair as a cookie, encoding the value with
// encodeURIComponent() in order to escape semicolons, commas, and spaces.
// If daysToLive is a number, set the max-age attribute so that the cookie
// expires after the specified number of days. Pass 0 to delete a cookie.
function setCookie(name, value, daysToLive = null) {
let cookie = `${name}=${encodeURIComponent(value)}`;
if (daysToLive !== null) {
cookie += `; max-age=${daysToLive * 60 * 60 * 24}`;
}
document.cookie = cookie;
}
/**
* The AbstractSet class defines a single abstract method, has().
*/
class AbstractSet {
// Throw an error here so that subclasses are forced
// to define their own working version of this method.
has(x) {
throw new Error("Abstract method");
}
}
/**
* NotSet is a concrete subclass of AbstractSet.
* The members of this set are all values that are not members of some
* other set. Because it is defined in terms of another set it is not
* writable, and because it has infinite members, it is not enumerable.
* All we can do with it is test for membership and convert it to a
* string using mathematical notation.
*/
class NotSet extends AbstractSet {
constructor(set) {
super();
this.set = set;
}
// Our implementation of the abstract method we inherited
has(x) {
return !this.set.has(x);
}
// And we also override this Object method
toString() {
return `{ x| x ∉ ${this.set.toString()} }`;
}
}
/**
* Range set is a concrete subclass of AbstractSet. Its members are
* all values that are between the from and to bounds, inclusive.
* Since its members can be floating point numbers, it is not
* enumerable and does not have a meaningful size.
*/
class RangeSet extends AbstractSet {
constructor(from, to) {
super();
this.from = from;
this.to = to;
}
has(x) {
return x >= this.from && x <= this.to;
}
toString() {
return `{ x| ${this.from} ≤ x ≤ ${this.to} }`;
}
}
/*
* AbstractEnumerableSet is an abstract subclass of AbstractSet. It defines
* an abstract getter that returns the size of the set and also defines an
* abstract iterator. And it then implements concrete isEmpty(), toString(),
* and equals() methods on top of those. Subclasses that implement the
* iterator, the size getter, and the has() method get these concrete
* methods for free.
*/
class AbstractEnumerableSet extends AbstractSet {
get size() {
throw new Error("Abstract method");
}
[Symbol.iterator]() {
throw new Error("Abstract method");
}
isEmpty() {
return this.size === 0;
}
toString() {
return `{${Array.from(this).join(", ")}}`;
}
equals(set) {
// If the other set is not also Enumerable, it isn't equal to this one
if (!(set instanceof AbstractEnumerableSet)) return false;
// If they don't have the same size, they're not equal
if (this.size !== set.size) return false;
// Loop through the elements of this set
for (let element of this) {
// If an element isn't in the other set, they aren't equal
if (!set.has(element)) return false;
}
// The elements matched, so the sets are equal
return true;
}
}
/*
* SingletonSet is a concrete subclass of AbstractEnumerableSet.
* A singleton set is a read-only set with a single member.
*/
class SingletonSet extends AbstractEnumerableSet {
constructor(member) {
super();
this.member = member;
}
// We implement these three methods, and inherit isEmpty, equals()
// and toString() implementations based on these methods.
has(x) {
return x === this.member;
}
get size() {
return 1;
}
*[Symbol.iterator]() {
yield this.member;
}
}
/*
* AbstractWritableSet is an abstract subclass of AbstractEnumerableSet.
* It defines the abstract methods insert() and remove() that insert and
* remove individual elements from the set, and then implements concrete
* add(), subtract(), and intersect() methods on top of those. Note that
* our API diverges here from the standard JavaScript Set class.
*/
class AbstractWritableSet extends AbstractEnumerableSet {
insert(x) {
throw new Error("Abstract method");
}
remove(x) {
throw new Error("Abstract method");
}
add(set) {
for (let element of set) {
this.insert(element);
}
}
subtract(set) {
for (let element of set) {
this.remove(element);
}
}
intersect(set) {
for (let element of this) {
if (!set.has(element)) {
this.remove(element);
}
}
}
}
/**
* A BitSet is a concrete subclass of AbstractWritableSet with a
* very efficient fixed-size set implementation for sets whose
* elements are non-negative integers less than some maximum size.
*/
class BitSet extends AbstractWritableSet {
constructor(max) {
super();
this.max = max; // The maximum integer we can store.
this.n = 0; // How many integers are in the set
this.numBytes = Math.floor(max / 8) + 1; // How many bytes we need
this.data = new Uint8Array(this.numBytes); // The bytes
}
// Internal method to check if a value is a legal member of this set
_valid(x) {
return Number.isInteger(x) && x >= 0 && x <= this.max;
}
// Tests whether the specified bit of the specified byte of our
// data array is set or not. Returns true or false.
_has(byte, bit) {
return (this.data[byte] & BitSet.bits[bit]) !== 0;
}
// Is the value x in this BitSet?
has(x) {
if (this._valid(x)) {
let byte = Math.floor(x / 8);
let bit = x % 8;
return this._has(byte, bit);
} else {
return false;
}
}
// Insert the value x into the BitSet
insert(x) {
if (this._valid(x)) {
// If the value is valid
let byte = Math.floor(x / 8); // convert to byte and bit
let bit = x % 8;
if (!this._has(byte, bit)) {
// If that bit is not set yet
this.data[byte] |= BitSet.bits[bit]; // then set it
this.n++; // and increment set size
}
} else {
throw new TypeError("Invalid set element: " + x);
}
}
remove(x) {
if (this._valid(x)) {
// If the value is valid
let byte = Math.floor(x / 8); // compute the byte and bit
let bit = x % 8;
if (this._has(byte, bit)) {
// If that bit is already set
this.data[byte] &= BitSet.masks[bit]; // then unset it
this.n--; // and decrement size
}
} else {
throw new TypeError("Invalid set element: " + x);
}
}
// A getter to return the size of the set
get size() {
return this.n;
}
// Iterate the set by just checking each bit in turn.
// (We could be a lot more clever and optimize this substantially)
*[Symbol.iterator]() {
for (let i = 0; i <= this.max; i++) {
if (this.has(i)) {
yield i;
}
}
}
}
// Some pre-computed values used by the has(), insert() and remove() methods
BitSet.bits = new Uint8Array([1, 2, 4, 8, 16, 32, 64, 128]);
BitSet.masks = new Uint8Array([~1, ~2, ~4, ~8, ~16, ~32, ~64, ~128]);
function setTheme(name) {
// Create a new <link rel="stylesheet"> element to load the named stylesheet
let link = document.createElement("link");
link.id = "theme";
link.rel = "stylesheet";
link.href = `themes/${name}.css`;
// Look for an existing link with id "theme"
let currentTheme = document.querySelector("#theme");
if (currentTheme) {
// If there is an existing theme, replace it with the new one.
currentTheme.replaceWith(link);
} else {
// Otherwise, just insert the link to the theme stylesheet.
document.head.append(link);
}
}
let authHeaders = new Headers();
// Don't use Basic auth unless it is over an HTTPS connection.
authHeaders.set("Authorization", `Basic ${btoa(`${username}:${password}`)}`);
fetch("/api/users/", { headers: authHeaders })
.then((response) => response.json()) // Error handling omitted...
.then((usersList) => displayAllUsers(usersList));
async function search(term) {
let url = new URL("/api/search");
url.searchParams.set("q", term);
let response = await fetch(url);
if (!response.ok) throw new Error(response.statusText);
let resultsArray = await response.json();
return resultsArray;
}
// Return the largest prime smaller than n, using the sieve of Eratosthenes
function sieve(n) {
let a = new Uint8Array(n + 1); // a[x] will be 1 if x is composite
let max = Math.floor(Math.sqrt(n)); // Don't do factors higher than this
let p = 2; // 2 is the first prime
while (p <= max) {
// For primes less than max
for (
let i = 2 * p;
i <= n;
i += p // Mark multiples of p as composite
)
a[i] = 1;
while (a[++p] /* empty */); // The next unmarked index is prime
}
while (a[n]) n--; // Loop backward to find the last prime
return n; // And return it
}
// Smear the pixels of the rectangle to the right, producing a
// sort of motion blur as if objects are moving from right to left.
// n must be 2 or larger. Larger values produce bigger smears.
// The rectangle is specified in the default coordinate system.
function smear(c, n, x, y, w, h) {
// Get the ImageData object that represents the rectangle of pixels to smear
let pixels = c.getImageData(x, y, w, h);
// This smear is done in-place and requires only the source ImageData.
// Some image processing algorithms require an additional ImageData to
// store transformed pixel values. If we needed an output buffer, we could
// create a new ImageData with the same dimensions like this:
// let output_pixels = c.createImageData(pixels);
// Get the dimensions of the grid of pixels in the ImageData object
let width = pixels.width,
height = pixels.height;
// This is the byte array that holds the raw pixel data, left-to-right and
// top-to-bottom. Each pixel occupies 4 consecutive bytes in R,G,B,A order.
let data = pixels.data;
// Each pixel after the first in each row is smeared by replacing it with
// 1/nth of its own value plus m/nths of the previous pixel's value
let m = n - 1;
for (let row = 0; row < height; row++) {
// For each row
let i = row * width * 4 + 4; // The offset of the second pixel of the row
for (let col = 1; col < width; col++, i += 4) {
// For each column
data[i] = (data[i] + data[i - 4] * m) / n; // Red pixel component
data[i + 1] = (data[i + 1] + data[i - 3] * m) / n; // Green
data[i + 2] = (data[i + 2] + data[i - 2] * m) / n; // Blue
data[i + 3] = (data[i + 3] + data[i - 1] * m) / n; // Alpha component
}
}
// Now copy the smeared image data back to the same position on the canvas
c.putImageData(pixels, x, y);
}
// This is the constructor function for our subclass
function Span(start, span) {
if (span >= 0) {
this.from = start;
this.to = start + span;
} else {
this.to = start;
this.from = start + span;
}
}
// Ensure that the Span prototype inherits from the Range prototype
Span.prototype = Object.create(Range.prototype);
// We don't want to inherit Range.prototype.constructor, so we
// define our own constructor property.
Span.prototype.constructor = Span;
// By defining its own toString() method, Span overrides the
// toString() method that it would otherwise inherit from Range.
Span.prototype.toString = function () {
return `(${this.from}... +${this.to - this.from})`;
};
const threads = require("worker_threads");
// The worker_threads module exports the boolean isMainThread property.
// This property is true when Node is running the main thread and it is
// false when Node is running a worker. We can use this fact to implement
// the main and worker threads in the same file.
if (threads.isMainThread) {
// If we're running in the main thread, then all we do is export
// a function. Instead of performing a computationally intensive
// task on the main thread, this function passes the task to a worker
// and returns a Promise that will resolve when the worker is done.
module.exports = function reticulateSplines(splines) {
return new Promise((resolve, reject) => {
// Create a worker that loads and runs this same file of code.
// Note the use of the special __filename variable.
let reticulator = new threads.Worker(__filename);
// Pass a copy of the splines array to the worker
reticulator.postMessage(splines);
// And then resolve or reject the Promise when we get
// a message or error from the worker.
reticulator.on("message", resolve);
reticulator.on("error", reject);
});
};
} else {
// If we get here, it means we're in the worker, so we register a
// handler to get messages from the main thread. This worker is designed
// to only receive a single message, so we register the event handler
// with once() instead of on(). This allows the worker to exit naturally
// when its work is complete.
threads.parentPort.once("message", (splines) => {
// When we get the splines from the parent thread, loop
// through them and reticulate all of them.
for (let spline of splines) {
// For the sake of example, assume that spline objects usually
// have a reticulate() method that does a lot of computation.
spline.reticulate ? spline.reticulate() : (spline.reticulated = true);
}
// When all the splines have (finally!) been reticulated
// pass a copy back to the main thread.
threads.parentPort.postMessage(splines);
});
}
// This is a simple static HTTP server that serves files from a specified
// directory. It also implements a special /test/mirror endpoint that
// echoes the incoming request, which can be useful when debugging clients.
const http = require("http"); // Use "https" if you have a certificate
const url = require("url"); // For parsing URLs
const path = require("path"); // For manipulating filesystem paths
const fs = require("fs"); // For reading files
// Serve files from the specified root directory via an HTTP server that
// listens on the specified port.
function serve(rootDirectory, port) {
let server = new http.Server(); // Create a new HTTP server
server.listen(port); // Listen on the specified port
console.log("Listening on port", port);
// When requests come in, handle them with this function
server.on("request", (request, response) => {
// Get the path portion of the request URL, ignoring
// any query parameters that are appended to it.
let endpoint = url.parse(request.url).pathname;
// If the request was for "/test/mirror", send back the request
// verbatim. Useful when you need to see the request headers and body.
if (endpoint === "/test/mirror") {
// Set response header
response.setHeader("Content-Type", "text/plain; charset=UTF-8");
// Specify response status code
response.writeHead(200); // 200 OK
// Begin the response body with the request
response.write(
`${request.method} ${request.url} HTTP/${request.httpVersion}\r\n`
);
// Output the request headers
let headers = request.rawHeaders;
for (let i = 0; i < headers.length; i += 2) {
response.write(`${headers[i]}: ${headers[i + 1]}\r\n`);
}
// End headers with an extra blank line
response.write("\r\n");
// Now we need to copy any request body to the response body
// Since they are both streams, we can use a pipe
request.pipe(response);
}
// Otherwise, serve a file from the local directory.
else {
// Map the endpoint to a file in the local filesystem
let filename = endpoint.substring(1); // strip leading /
// Don't allow "../" in the path because it would be a security
// hole to serve anything outside the root directory.
filename = filename.replace(/\.\.\//g, "");
// Now convert from relative to absolute filename
filename = path.resolve(rootDirectory, filename);
// Now guess the type file's content type based on extension
let type;
switch (path.extname(filename)) {
case ".html":
case ".htm":
type = "text/html";
break;
case ".js":
type = "text/javascript";
break;
case ".css":
type = "text/css";
break;
case ".png":
type = "image/png";
break;
case ".txt":
type = "text/plain";
break;
default:
type = "application/octet-stream";
break;
}
let stream = fs.createReadStream(filename);
stream.once("readable", () => {
// If the stream becomes readable, then set the
// Content-Type header and a 200 OK status. Then pipe the
// file reader stream to the response. The pipe will
// automatically call response.end() when the stream ends.
response.setHeader("Content-Type", type);
response.writeHead(200);
stream.pipe(response);
});
stream.on("error", (err) => {
// Instead, if we get an error trying to open the stream
// then the file probably does not exist or is not readable.
// Send a 404 Not Found plain-text response with the
// error message.
response.setHeader("Content-Type", "text/plain; charset=UTF-8");
response.writeHead(404);
response.end(err.message);
});
}
});
}
// When we're invoked from the command line, call the serve() function
serve(process.argv[2] || "/tmp", parseInt(process.argv[3]) || 8000);
// This is how we could define a stats module
const stats = (function () {
// Utility functions private to the module
const sum = (x, y) => x + y;
const square = (x) => x * x;
// A public function that will be exported
function mean(data) {
return data.reduce(sum) / data.length;
}
// A public function that we will export
function stddev(data) {
let m = mean(data);
return Math.sqrt(
data
.map((x) => x - m)
.map(square)
.reduce(sum) /
(data.length - 1)
);
}
// We export the public function as properties of an object
return { mean, stddev };
})();
// And here is how we might use the module
stats.mean([1, 3, 5, 7, 9]); // => 5
stats.stddev([1, 3, 5, 7, 9]); // => Math.sqrt(10)
let data = [1, 1, 3, 5, 5]; // This is our array of numbers
// The mean is the sum of the elements divided by the number of elements
let total = 0;
for (let i = 0; i < data.length; i++) total += data[i];
let mean = total / data.length; // mean == 3; The mean of our data is 3
// To compute the standard deviation, we first sum the squares of
// the deviation of each element from the mean.
total = 0;
for (let i = 0; i < data.length; i++) {
let deviation = data[i] - mean;
total += deviation * deviation;
}
let stddev = Math.sqrt(total / (data.length - 1)); // stddev == 2
// First, define two simple functions
const sum = (x, y) => x + y;
const square = (x) => x * x;
// Then use those functions with Array methods to compute mean and stddev
let data = [1, 1, 3, 5, 5];
let mean = data.reduce(sum) / data.length; // mean == 3
let deviations = data.map((x) => x - mean);
let stddev = Math.sqrt(deviations.map(square).reduce(sum) / (data.length - 1));
stddev; // => 2
const map = function (a, ...args) {
return a.map(...args);
};
const reduce = function (a, ...args) {
return a.reduce(...args);
};
const sum = (x, y) => x + y;
const square = (x) => x * x;
let data = [1, 1, 3, 5, 5];
let mean = reduce(data, sum) / data.length;
let deviations = map(data, (x) => x - mean);
let stddev = Math.sqrt(
reduce(map(deviations, square), sum) / (data.length - 1)
);
stddev; // => 2
/**
* An asynchronous function for streaming the body of a Response object
* obtained from a fetch() request. Pass the Response object as the first
* argument followed by two optional callbacks.
*
* If you specify a function as the second argument, that reportProgress
* callback will be called once for each chunk that is received. The first
* argument passed is the total number of bytes received so far. The second
* argument is a number between 0 and 1 specifying how complete the download
* is. If the Response object has no "Content-Length" header, however, then
* this second argument will always be NaN.
*
* If you want to process the data in chunks as they arrive, specify a
* function as the third argument. The chunks will be passed, as Uint8Array
* objects, to this processChunk callback.
*
* streamBody() returns a Promise that resolves to a string. If a processChunk
* callback was supplied then this string is the concatenation of the values
* returned by that callback. Otherwise the string is the concatenation of
* the chunk values converted to UTF-8 strings.
*/
async function streamBody(response, reportProgress, processChunk) {
// How many bytes are we expecting, or NaN if no header
let expectedBytes = parseInt(response.headers.get("Content-Length"));
let bytesRead = 0; // How many bytes received so far
let reader = response.body.getReader(); // Read bytes with this function
let decoder = new TextDecoder("utf-8"); // For converting bytes to text
let body = ""; // Text read so far
while (true) {
// Loop until we exit below
let { done, value } = await reader.read(); // Read a chunk
if (value) {
// If we got a byte array:
if (processChunk) {
// Process the bytes if
let processed = processChunk(value); // a callback was passed.
if (processed) {
body += processed;
}
} else {
// Otherwise, convert bytes
body += decoder.decode(value, { stream: true }); // to text.
}
if (reportProgress) {
// If a progress callback was
bytesRead += value.length; // passed, then call it
reportProgress(bytesRead, bytesRead / expectedBytes);
}
}
if (done) {
// If this is the last chunk,
break; // exit the loop
}
}
return body; // Return the body text we accumulated
}
// Yield the first n elements of the specified iterable object
function* take(n, iterable) {
let it = iterable[Symbol.iterator](); // Get iterator for iterable object
while (n-- > 0) {
// Loop n times:
let next = it.next(); // Get the next item from the iterator.
if (next.done) return;
// If there are no more values, return early
else yield next.value; // otherwise, yield the value
}
}
// An array of the first 5 Fibonacci numbers
[...take(5, fibonacciSequence())]; // => [1, 1, 2, 3, 5]
// Return the plain-text content of element e, recursing into child elements.
// This method works like the textContent property
function textContent(e) {
let s = ""; // Accumulate the text here
for (let child = e.firstChild; child !== null; child = child.nextSibling) {
let type = child.nodeType;
if (type === 3) {
// If it is a Text node
s += child.nodeValue; // add the text content to our string.
} else if (type === 1) {
// And if it is an Element node
s += textContent(child); // then recurse.
}
}
return s;
}
// This function takes a function and returns a wrapped version
function timed(f) {
return function (...args) {
// Collect args into a rest parameter array
console.log(`Entering function ${f.name}`);
let startTime = Date.now();
try {
// Pass all of our arguments to the wrapped function
return f(...args); // Spread the args back out again
} finally {
// Before we return the wrapped return value, print elapsed time.
console.log(`Exiting ${f.name} after ${Date.now() - startTime}ms`);
}
};
}
// Compute the sum of the numbers between 1 and n by brute force
function benchmark(n) {
let sum = 0;
for (let i = 1; i <= n; i++) sum += i;
return sum;
}
// Now invoke the timed version of that test function
timed(benchmark)(1000000); // => 500000500000; this is the sum of the numbers
/**
* TOC.js: create a table of contents for a document.
*
* This script runs when the DOMContentLoaded event is fired and
* automatically generates a table of contents for the document.
* It does not define any global symbols so it should not conflict
* with other scripts.
*
* When this script runs, it first looks for a document element with
* an id of "TOC". If there is no such element it creates one at the
* start of the document. Next, the function finds all <h2> through
* <h6> tags, treats them as section titles, and creates a table of
* contents within the TOC element. The function adds section numbers
* to each section heading and wraps the headings in named anchors so
* that the TOC can link to them. The generated anchors have names
* that begin with "TOC", so you should avoid this prefix in your own
* HTML.
*
* The entries in the generated TOC can be styled with CSS. All
* entries have a class "TOCEntry". Entries also have a class that
* corresponds to the level of the section heading. <h1> tags generate
* entries of class "TOCLevel1", <h2> tags generate entries of class
* "TOCLevel2", and so on. Section numbers inserted into headings have
* class "TOCSectNum".
*
* You might use this script with a stylesheet like this:
*
* #TOC { border: solid black 1px; margin: 10px; padding: 10px; }
* .TOCEntry { margin: 5px 0px; }
* .TOCEntry a { text-decoration: none; }
* .TOCLevel1 { font-size: 16pt; font-weight: bold; }
* .TOCLevel2 { font-size: 14pt; margin-left: .25in; }
* .TOCLevel3 { font-size: 12pt; margin-left: .5in; }
* .TOCSectNum:after { content: ": "; }
*
* To hide the section numbers, use this:
*
* .TOCSectNum { display: none }
**/
document.addEventListener("DOMContentLoaded", () => {
// Find the TOC container element.
// If there isn't one, create one at the start of the document.
let toc = document.querySelector("#TOC");
if (!toc) {
toc = document.createElement("div");
toc.id = "TOC";
document.body.prepend(toc);
}
// Find all section heading elements. We're assuming here that the
// document title uses <h1> and that sections within the document are
// marked with <h2> through <h6>.
let headings = document.querySelectorAll("h2,h3,h4,h5,h6");
// Initialize an array that keeps track of section numbers.
let sectionNumbers = [0, 0, 0, 0, 0];
// Now loop through the section header elements we found.
for (let heading of headings) {
// Skip the heading if it is inside the TOC container.
if (heading.parentNode === toc) {
continue;
}
// Figure out what level heading it is.
// Subtract 1 because <h2> is a level-1 heading.
let level = parseInt(heading.tagName.charAt(1)) - 1;
// Increment the section number for this heading level
// and reset all lower heading level numbers to zero.
sectionNumbers[level - 1]++;
for (let i = level; i < sectionNumbers.length; i++) {
sectionNumbers[i] = 0;
}
// Now combine section numbers for all heading levels
// to produce a section number like 2.3.1.
let sectionNumber = sectionNumbers.slice(0, level).join(".");
// Add the section number to the section header title.
// We place the number in a <span> to make it styleable.
let span = document.createElement("span");
span.className = "TOCSectNum";
span.textContent = sectionNumber;
heading.prepend(span);
// Wrap the heading in a named anchor so we can link to it.
let anchor = document.createElement("a");
let fragmentName = `TOC${sectionNumber}`;
anchor.name = fragmentName;
heading.before(anchor); // Insert anchor before heading
anchor.append(heading); // and move heading inside anchor
// Now create a link to this section.
let link = document.createElement("a");
link.href = `#${fragmentName}`; // Link destination
// Copy the heading text into the link. This is a safe use of
// innerHTML because we are not inserting any untrusted strings.
link.innerHTML = heading.innerHTML;
// Place the link in a div that is styleable based on the level.
let entry = document.createElement("div");
entry.classList.add("TOCEntry", `TOCLevel${level}`);
entry.append(link);
// And add the div to the TOC container.
toc.append(entry);
}
});
// Replace the method named m of the object o with a version that logs
// messages before and after invoking the original method.
function trace(o, m) {
let original = o[m]; // Remember original method in the closure.
o[m] = function (...args) {
// Now define the new method.
console.log(new Date(), "Entering:", m); // Log message.
let result = original.apply(this, args); // Invoke original.
console.log(new Date(), "Exiting:", m); // Log message.
return result; // Return result.
};
}
// Shear transform:
// x' = x + kx*y;
// y' = ky*x + y;
function shear(c, kx, ky) {
c.transform(1, ky, kx, 1, 0, 0);
}
// Rotate theta radians counterclockwise around the point (x,y)
// This can also be accomplished with a translate, rotate, translate sequence
function rotateAbout(c, theta, x, y) {
let ct = Math.cos(theta);
let st = Math.sin(theta);
c.transform(ct, -st, st, ct, -x * ct - y * st + x, x * st - y * ct + y);
}
// Recursively traverse the Document or Element e, invoking the function
// f on e and on each of its descendants
function traverse(e, f) {
f(e); // Invoke f() on e
for (let child of e.children) {
// Iterate over the children
traverse(child, f); // And recurse on each one
}
}
function traverse2(e, f) {
f(e); // Invoke f() on e
let child = e.firstElementChild; // Iterate the children linked-list style
while (child !== null) {
traverse2(child, f); // And recurse
child = child.nextElementSibling;
}
}
class TypedMap extends Map {
constructor(keyType, valueType, entries) {
// If entries are specified, check their types
if (entries) {
for (let [k, v] of entries) {
if (typeof k !== keyType || typeof v !== valueType) {
throw new TypeError(`Wrong type for entry [${k}, ${v}]`);
}
}
}
// Initialize the superclass with the (type-checked) initial entries
super(entries);
// And then initialize this subclass by storing the types
this.keyType = keyType;
this.valueType = valueType;
}
// Now redefine the set() method to add type checking for any
// new entries added to the map.
set(key, value) {
// Throw an error if the key or value are of the wrong type
if (this.keyType && typeof key !== this.keyType) {
throw new TypeError(`${key} is not of type ${this.keyType}`);
}
if (this.valueType && typeof value !== this.valueType) {
throw new TypeError(`${value} is not of type ${this.valueType}`);
}
// If the types are correct, we invoke the superclass's version of
// the set() method, to actually add the entry to the map. And we
// return whatever the superclass method returns.
return super.set(key, value);
}
}
// Initialize the counter property of the function object.
// Function declarations are hoisted so we really can
// do this assignment before the function declaration.
uniqueInteger.counter = 0;
// This function returns a different integer each time it is called.
// It uses a property of itself to remember the next value to be returned.
function uniqueInteger() {
return uniqueInteger.counter++; // Return and increment counter property
}
uniqueInteger(); // => 0
uniqueInteger(); // => 1
let uniqueInteger = (function () {
// Define and invoke
let counter = 0; // Private state of function below
return function () {
return counter++;
};
})();
uniqueInteger(); // => 0
uniqueInteger(); // => 1
// The canvas.toBlob() function is callback-based.
// This is a Promise-based wrapper for it.
async function getCanvasBlob(canvas) {
return new Promise((resolve, reject) => {
canvas.toBlob(resolve);
});
}
// Here is how we upload a PNG file from a canvas
async function uploadCanvasImage(canvas) {
let pngblob = await getCanvasBlob(canvas);
let formdata = new FormData();
formdata.set("canvasimage", pngblob);
let response = await fetch("/upload", { method: "POST", body: formdata });
let body = await response.json();
}
function wait(duration) {
// Create and return a new Promise
return new Promise((resolve, reject) => {
// These control the Promise
// If the argument is invalid, reject the Promise
if (duration < 0) {
reject(new Error("Time travel not yet implemented"));
}
// Otherwise, wait asynchronously and then resolve the Promise.
// setTimeout will invoke resolve() with no arguments, which means
// that the Promise will fulfill with the undefined value.
setTimeout(resolve, duration);
});
}
// Begin by creating an audioContext object. Safari still requires
// us to use webkitAudioContext instead of AudioContext.
let audioContext = new (this.AudioContext || this.webkitAudioContext)();
// Define the base sound as a combination of three pure sine waves
let notes = [293.7, 370.0, 440.0]; // D major chord: D, F# and A
// Create oscillator nodes for each of the notes we want to play
let oscillators = notes.map((note) => {
let o = audioContext.createOscillator();
o.frequency.value = note;
return o;
});
// Shape the sound by controlling its volume over time.
// Starting at time 0 quickly ramp up to full volume.
// Then starting at time 0.1 slowly ramp down to 0.
let volumeControl = audioContext.createGain();
volumeControl.gain.setTargetAtTime(1, 0.0, 0.02);
volumeControl.gain.setTargetAtTime(0, 0.1, 0.2);
// We're going to send the sound to the default destination:
// the user's speakers
let speakers = audioContext.destination;
// Connect each of the source notes to the volume control
oscillators.forEach((o) => o.connect(volumeControl));
// And connect the output of the volume control to the speakers.
volumeControl.connect(speakers);
// Now start playing the sounds and let them run for 1.25 seconds.
let startTime = audioContext.currentTime;
let stopTime = startTime + 1.25;
oscillators.forEach((o) => {
o.start(startTime);
o.stop(stopTime);
});
// If we want to create a sequence of sounds we can use event handlers
oscillators[0].addEventListener("ended", () => {
// This event handler is invoked when the note stops playing
});
function words(s) {
var r = /\s+|$/g; // Match one or more spaces or end
r.lastIndex = s.match(/[^ ]/).index; // Start matching at first nonspace
return {
// Return an iterable iterator object
[Symbol.iterator]() {
// This makes us iterable
return this;
},
next() {
// This makes us an iterator
let start = r.lastIndex; // Resume where the last match ended
if (start < s.length) {
// If we're not done
let match = r.exec(s); // Match the next word boundary
if (match) {
// If we found one, return the word
return { value: s.substring(start, match.index) };
}
}
return { done: true }; // Otherwise, say that we're done
},
};
}
[...words(" abc def ghi! ")]; // => ["abc", "def", "ghi!"]
function write(stream, chunk, callback) {
// Write the specified chunk to the specified stream
let hasMoreRoom = stream.write(chunk);
// Check the return value of the write() method:
if (hasMoreRoom) {
// If it returned true, then
setImmediate(callback); // invoke callback asynchronously.
} else {
// If it returned false, then
stream.once("drain", callback); // invoke callback on drain event.
}
}
// Given an array of iterables, yield their elements in interleaved order.
function* zip(...iterables) {
// Get an iterator for each iterable
let iterators = iterables.map((i) => i[Symbol.iterator]());
let index = 0;
while (iterators.length > 0) {
// While there are still some iterators
if (index >= iterators.length) {
// If we reached the last iterator
index = 0; // go back to the first one.
}
let item = iterators[index].next(); // Get next item from next iterator.
if (item.done) {
// If that iterator is done
iterators.splice(index, 1); // then remove it from the array.
} else {
// Otherwise,
yield item.value; // yield the iterated value
index++; // and move on to the next iterator.
}
}
}
// Interleave three iterable objects
[...zip(oneDigitPrimes(), "ab", [0])]; // => [2,"a",0,3,"b",5,7]
// This utility function asynchronously obtains the database object (creating
// and initializing the DB if necessary) and passes it to the callback.
function withDB(callback) {
let request = indexedDB.open("zipcodes", 1); // Request v1 of the database
request.onerror = console.error; // Log any errors
request.onsuccess = () => {
// Or call this when done
let db = request.result; // The result of the request is the database
callback(db); // Invoke the callback with the database
};
// If version 1 of the database does not yet exist, then this event
// handler will be triggered. This is used to create and initialize
// object stores and indexes when the DB is first created or to modify
// them when we switch from one version of the DB schema to another.
request.onupgradeneeded = () => {
initdb(request.result, callback);
};
}
// withDB() calls this function if the database has not been initialized yet.
// We set up the database and populate it with data, then pass the database to
// the callback function.
//
// Our zip code database includes one object store that holds objects like this:
//
// {
// zipcode: "02134",
// city: "Allston",
// state: "MA",
// }
//
// We use the "zipcode" property as the database key and create an index for
// the city name.
function initdb(db, callback) {
// Create the object store, specifying a name for the store and
// an options object that includes the "key path" specifying the
// property name of the key field for this store.
let store = db.createObjectStore(
"zipcodes", // store name
{ keyPath: "zipcode" }
);
// Now index the object store by city name as well as by zip code.
// With this method the key path string is passed directly as a
// required argument rather than as part of an options object.
store.createIndex("cities", "city");
// Now get the data we are going to initialize the database with.
// The zipcodes.json data file was generated from CC-licensed data from
// www.geonames.org: https://download.geonames.org/export/zip/US.zip
fetch("zipcodes.json") // Make an HTTP GET request
.then((response) => response.json()) // Parse the body as JSON
.then((zipcodes) => {
// Get 40K zip code records
// In order to insert zip code data into the database we need a
// transaction object. To create our transaction object, we need
// to specify which object stores we'll be using (we only have
// one) and we need to tell it that we'll be doing writes to the
// database, not just reads:
let transaction = db.transaction(["zipcodes"], "readwrite");
transaction.onerror = console.error;
// Get our object store from the transaction
let store = transaction.objectStore("zipcodes");
// The best part about the IndexedDB API is that object stores
// are *really* simple. Here's how we add (or update) our records:
for (let record of zipcodes) {
store.put(record);
}
// When the transaction completes successfully, the database
// is initialized and ready for use, so we can call the
// callback function that was originally passed to withDB()
transaction.oncomplete = () => {
callback(db);
};
});
}
// Given a zip code, use the IndexedDB API to asynchronously look up the city
// with that zip code, and pass it to the specified callback, or pass null if
// no city is found.
export function lookupCity(zip, callback) {
withDB((db) => {
// Create a read-only transaction object for this query. The
// argument is an array of object stores we will need to use.
let transaction = db.transaction(["zipcodes"]);
// Get the object store from the transaction
let zipcodes = transaction.objectStore("zipcodes");
// Now request the object that matches the specified zipcode key.
// The lines above were synchronous, but this one is async.
let request = zipcodes.get(zip);
request.onerror = console.error; // Log errors
request.onsuccess = () => {
// Or call this function on success
let record = request.result; // This is the query result
if (record) {
// If we found a match, pass it to the callback
callback(`${record.city}, ${record.state}`);
} else {
// Otherwise, tell the callback that we failed
callback(null);
}
};
});
}
// Given the name of a city, use the IndexedDB API to asynchronously
// look up all zip code records for all cities (in any state) that have
// that (case-sensitive) name.
export function lookupZipcodes(city, callback) {
withDB((db) => {
// As above, we create a transaction and get the object store
let transaction = db.transaction(["zipcodes"]);
let store = transaction.objectStore("zipcodes");
// This time we also get the city index of the object store
let index = store.index("cities");
// Ask for all matching records in the index with the specified
// city name, and when we get them we pass them to the callback.
// If we expected more results, we might use openCursor() instead.
let request = index.getAll(city);
request.onerror = console.error;
request.onsuccess = () => {
callback(request.result);
};
});
}
import { lookupCity, lookupZipcodes } from "./zipcodeDatabase.js";
window.addEventListener("load", () => {
let zipcodeInput = document.querySelector("#zipcodeInput");
let cityOutput = document.querySelector("#cityOutput");
zipcodeInput.onchange = () => {
lookupCity(zipcodeInput.value, (city) => {
cityOutput.value = city || "Unknown zip code";
});
};
let cityInput = document.querySelector("#cityInput");
let zipcodesOutput = document.querySelector("#zipcodesOutput");
cityInput.onchange = () => {
zipcodesOutput.textContent = "Matching zipcodes:";
lookupZipcodes(cityInput.value, (zipcodes) => {
zipcodes.forEach((zip) => {
let item = document.createElement("li");
item.append(`${zip.zipcode}: ${zip.city}, ${zip.state}`);
zipcodesOutput.append(item);
});
});
};
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment