diff --git a/package-lock.json b/package-lock.json
index 7c73e2f1..c341d949 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -61,6 +61,7 @@
"react-dom": "^15.7.0",
"redis": "^0.10.3",
"request": "^2.88.2",
+ "request-filtering-agent": "^3.2.0",
"requirejs": "2.1.14",
"s-expression": "~2.2.0",
"script-loader": "^0.7.2",
@@ -77,7 +78,7 @@
"webpack": "^5.89.0"
},
"devDependencies": {
- "chromedriver": "^141.0.1",
+ "chromedriver": "^146.0.4",
"selenium-webdriver": "^3.6.0",
"webpack-cli": "^5.1.4"
}
@@ -8549,14 +8550,14 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
- "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
+ "version": "1.13.6",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
+ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.4",
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
@@ -10031,19 +10032,19 @@
}
},
"node_modules/chromedriver": {
- "version": "141.0.1",
- "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-141.0.1.tgz",
- "integrity": "sha512-BvBP/wlZDU/oDSQ7cbolKE2DI/PP2T2qDWN75+QiPkW5bUs/pd5uz4LYREl1fyoIerhLGhS0OSmMxpUfDbP4Tg==",
+ "version": "146.0.4",
+ "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-146.0.4.tgz",
+ "integrity": "sha512-/yNqo99Xm0qgAjSh7DYy2KX6pfgyGnIM/k+2jUSc0T6CUCP5k26DWrAYU7Sy0t/dKCntMl45FDWfqP+yRWaCZQ==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@testim/chrome-version": "^1.1.4",
- "axios": "^1.12.0",
+ "axios": "^1.13.5",
"compare-versions": "^6.1.0",
"extract-zip": "^2.0.1",
- "proxy-agent": "^6.4.0",
- "proxy-from-env": "^1.1.0",
+ "proxy-agent": "^6.5.0",
+ "proxy-from-env": "^2.0.0",
"tcp-port-used": "^1.0.2"
},
"bin": {
@@ -10053,6 +10054,16 @@
"node": ">=20"
}
},
+ "node_modules/chromedriver/node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/cipher-base": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz",
@@ -12954,9 +12965,9 @@
"license": "ISC"
},
"node_modules/follow-redirects": {
- "version": "1.15.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
- "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
@@ -13016,9 +13027,9 @@
}
},
"node_modules/form-data": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
- "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -20013,6 +20024,27 @@
"node": ">= 6"
}
},
+ "node_modules/request-filtering-agent": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/request-filtering-agent/-/request-filtering-agent-3.2.0.tgz",
+ "integrity": "sha512-tKPrKdsmTFuGG1/pBEpzTB66mDZ2lZLW8kjW4N6jj4QjnxUTKrIfv5p2zuJRfztOos86jRPD41lRaGjh+1QqDw==",
+ "license": "MIT",
+ "dependencies": {
+ "ipaddr.js": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/request-filtering-agent/node_modules/ipaddr.js": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
+ "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/request/node_modules/form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
diff --git a/package.json b/package.json
index 07406f4b..55646236 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
"react-dom": "^15.7.0",
"redis": "^0.10.3",
"request": "^2.88.2",
+ "request-filtering-agent": "^3.2.0",
"requirejs": "2.1.14",
"s-expression": "~2.2.0",
"script-loader": "^0.7.2",
@@ -88,7 +89,7 @@
"author": "Joe Politz",
"license": "Apache-2.0",
"devDependencies": {
- "chromedriver": "^141.0.1",
+ "chromedriver": "^146.0.4",
"selenium-webdriver": "^3.6.0",
"webpack-cli": "^5.1.4"
}
diff --git a/src/google-auth.js b/src/google-auth.js
index 7357b137..4dc92308 100644
--- a/src/google-auth.js
+++ b/src/google-auth.js
@@ -4,12 +4,14 @@ var OAuth2 = gapi.auth.OAuth2;
var DEFAULT_OAUTH_SCOPES = [
"email",
+ "profile",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/drive.install",
];
var FULL_OAUTH_SCOPES = [
"email",
+ "profile",
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/drive",
diff --git a/src/run.js b/src/run.js
index e5fcac29..f80254cb 100644
--- a/src/run.js
+++ b/src/run.js
@@ -23,6 +23,8 @@ var res = Q.fcall(function(db) {
development: process.env["NODE_ENV"] !== "production",
baseUrl: process.env["BASE_URL"],
logURL: process.env["LOG_URL"],
+ logUser: process.env["LOG_USER"],
+ logPassword: process.env["LOG_PASSWORD"],
gitRev: process.env["GIT_REV"] || git.short(),
gitBranch: process.env["GIT_BRANCH"] || git.branch(),
port: process.env["PORT"],
diff --git a/src/server.js b/src/server.js
index 75a40d57..9395dab6 100644
--- a/src/server.js
+++ b/src/server.js
@@ -7,7 +7,31 @@ const { drive } = require("googleapis/build/src/apis/drive/index.js");
var BACKREF_KEY = "originalProgram";
+// Limits for the streaming proxy. /downloadImg gets larger/looser caps because
+// images can legitimately be tens of MB; also we've seen e.g. Drive ?export=
+// take a while to get going. SHAREURL is intended to always be program
+// plaintext.
+// NOTE(joe + claude): really the timeout maybe should be on idleness at
+// startup/between bytes, not overall per completed request, but that's work to
+// plumb into `request`
+var IMAGE_PROXY_MAX_BYTES = 20 * 1024 * 1024; // 20 MB
+var IMAGE_PROXY_TIMEOUT_MS = 30 * 1000; // 30 s
+var SHAREURL_PROXY_MAX_BYTES = 1 * 1024 * 1024; // 1 MB
+var SHAREURL_PROXY_TIMEOUT_MS = 10 * 1000; // 10 s
+
function start(config, onServerReady) {
+ var defaultOpts = {
+ PYRET: process.env.PYRET,
+ BASE_URL: config.baseUrl,
+ GOOGLE_API_KEY: config.google.apiKey,
+ GOOGLE_APP_ID: config.google.appId,
+ LOG_URL: config.logURL,
+ LOG_PASSWORD: config.logPassword,
+ LOG_USER: config.logUser,
+ GIT_REV : config.gitRev,
+ GIT_BRANCH: config.gitBranch,
+ POSTMESSAGE_ORIGIN: process.env.POSTMESSAGE_ORIGIN
+ };
var express = require('express');
var cookieSession = require('cookie-session');
var cookieParser = require('cookie-parser');
@@ -15,6 +39,7 @@ function start(config, onServerReady) {
var csrf = require('csurf');
var googleAuth = require('./google-auth.js');
var request = require('request');
+ var requestFilteringAgent = require('request-filtering-agent');
var mustache = require('mustache-express');
var url = require('url');
var fs = require('fs');
@@ -107,14 +132,8 @@ function start(config, onServerReady) {
app.get("/", function(req, res) {
var content = loggedIn(req) ? "My Programs" : "Log In";
- res.render("index.html", {
- PYRET: process.env.PYRET,
+ res.render("index.html", { ...defaultOpts,
LEFT_LINK: content,
- GOOGLE_API_KEY: config.google.apiKey,
- BASE_URL: config.baseUrl,
- LOG_URL: config.logURL,
- GIT_REV : config.gitRev,
- GIT_BRANCH: config.gitBranch
});
});
@@ -180,24 +199,74 @@ function start(config, onServerReady) {
});
}
- app.get("/downloadImg", function(req, response) {
- var parsed = url.parse(req.url);
- var googleLink = decodeURIComponent(parsed.query.slice(0));
- var googleParsed = url.parse(googleLink);
- var gReq = request({url: googleLink, encoding: 'binary'}, function(error, imgResponse, body) {
- if(error) {
- response.status(400).send({type: "image-load-failure", error: "Unable to load image " + String(error)});
+ function proxyStreamFetch(opts) {
+ var res = opts.res;
+ res.set('X-Content-Type-Options', 'nosniff');
+ res.set('Content-Security-Policy', 'sandbox');
+
+ var parsed;
+ try { parsed = new URL(opts.url); }
+ catch (e) { return res.status(400).send({ error: 'invalid-url' }); }
+ if (opts.allowedHosts && !opts.allowedHosts(parsed.hostname)) {
+ return res.status(400).send({ error: 'host-not-allowed' });
+ }
+
+ var bytes = 0;
+ var upstream = request({
+ url: opts.url,
+ timeout: opts.timeoutMs,
+ agent: requestFilteringAgent.useAgent(opts.url),
+ followRedirect: function(resp) {
+ if (!opts.allowedHosts) return true;
+ try {
+ var next = new URL(resp.headers.location, opts.url);
+ return opts.allowedHosts(next.hostname);
+ } catch (_) { return false; }
+ },
+ });
+ // If the client disconnects (e.g. the browser aborts /load-shareurl after
+ // direct succeeded), tear down the upstream connection too — otherwise
+ // we'd keep streaming bytes from raw.githubusercontent.com to nowhere.
+ res.on('close', function() { upstream.destroy(); });
+ upstream.on('error', function(err) {
+ if (!res.headersSent) opts.onError(res, err);
+ });
+ upstream.on('response', function(upRes) {
+ if (opts.contentTypeOk && !opts.contentTypeOk(upRes.headers['content-type'])) {
+ upstream.destroy();
+ return res.status(400).send({ error: 'content-type-not-allowed', detail: upRes.headers['content-type'] });
}
- else {
- var h = imgResponse.headers;
- var ct = h['content-type'];
- if((!ct) || (ct.indexOf('image/') !== 0)) {
- response.status(400).send({type: "non-image", error: "Invalid image type " + ct});
- return;
- }
- response.set('content-type', ct);
- response.end(body, 'binary');
+ res.status(upRes.statusCode);
+ if (upRes.headers['content-type']) {
+ res.set('content-type', upRes.headers['content-type']);
}
+ upRes.on('data', function(chunk) {
+ bytes += chunk.length;
+ if (bytes > opts.maxBytes) {
+ upstream.destroy();
+ if (!res.headersSent) res.status(502).send({ error: 'too-large' });
+ else res.destroy();
+ }
+ });
+ // Pipe upRes (IncomingMessage), not upstream (request object). The
+ // request library's .pipe copies upstream headers verbatim, which
+ // would overwrite the security headers set above.
+ upRes.pipe(res);
+ });
+ }
+
+ app.get("/downloadImg", function(req, response) {
+ var googleLink = decodeURIComponent(url.parse(req.url).query.slice(0));
+ proxyStreamFetch({
+ res: response,
+ url: googleLink,
+ allowedHosts: null,
+ maxBytes: IMAGE_PROXY_MAX_BYTES,
+ timeoutMs: IMAGE_PROXY_TIMEOUT_MS,
+ contentTypeOk: function(ct) { return ct && ct.indexOf('image/') === 0; },
+ onError: function(res, err) {
+ res.status(400).send({ type: 'image-load-failure', error: 'Unable to load image ' + String(err) });
+ },
});
});
@@ -529,30 +598,14 @@ function start(config, onServerReady) {
});
app.get("/editor", function(req, res) {
- res.render("editor.html", {
- PYRET: process.env.PYRET,
- BASE_URL: config.baseUrl,
- GOOGLE_API_KEY: config.google.apiKey,
- GOOGLE_APP_ID: config.google.appId,
+ res.render("editor.html", { ...defaultOpts,
CSRF_TOKEN: req.csrfToken(),
- LOG_URL: config.logURL,
- GIT_REV : config.gitRev,
- GIT_BRANCH: config.gitBranch,
- POSTMESSAGE_ORIGIN: process.env.POSTMESSAGE_ORIGIN
});
});
app.get("/blocks", function(req, res) {
- res.render("blocks.html", {
- PYRET: process.env.PYRET,
- BASE_URL: config.baseUrl,
- GOOGLE_API_KEY: config.google.apiKey,
- GOOGLE_APP_ID: config.google.appId,
+ res.render("blocks.html", { ...defaultOpts,
CSRF_TOKEN: req.csrfToken(),
- LOG_URL: config.logURL,
- GIT_REV : config.gitRev,
- GIT_BRANCH: config.gitBranch,
- POSTMESSAGE_ORIGIN: process.env.POSTMESSAGE_ORIGIN
});
});
@@ -575,6 +628,26 @@ function start(config, onServerReady) {
});
+ // Server-side proxy for #shareurl loads from hosts that some school networks
+ // block or will likely block (notably raw.githubusercontent.com).
+ // Eager-proxied client-side for any URL whose host is in
+ // SHAREURL_ALLOWED_HOSTS. We can expand this list as needed.
+ var SHAREURL_ALLOWED_HOSTS = new Set(['raw.githubusercontent.com']);
+
+ app.get("/load-shareurl", function(req, res) {
+ proxyStreamFetch({
+ res: res,
+ url: req.query.url,
+ allowedHosts: function(h) { return SHAREURL_ALLOWED_HOSTS.has(h); },
+ maxBytes: SHAREURL_PROXY_MAX_BYTES,
+ timeoutMs: SHAREURL_PROXY_TIMEOUT_MS,
+ contentTypeOk: null,
+ onError: function(res, err) {
+ res.status(502).send({ error: 'upstream-error' });
+ },
+ });
+ });
+
app.post("/share-image", function(req, res) {
var driveFileId = req.body.fileId;
diff --git a/src/web/css/editor.css b/src/web/css/editor.css
index 6cea932b..424288b1 100644
--- a/src/web/css/editor.css
+++ b/src/web/css/editor.css
@@ -486,7 +486,7 @@ div.repl-animation {
div.trace {
overflow-x: auto;
- padding-top: 1px;
+ margin-top: 1px;
}
div.trace.error {
background-color: var(--trace-err-bg);
@@ -2304,23 +2304,42 @@ table.pyret-table.pyret-matrix {
}
-table.pyret-table { width: 98%; overflow: scroll; }
+table.pyret-table { width: 98%; overflow: scroll; border: none; }
+/* make sure rotated elts in the table (like roughnums!) don't punch through */
table.pyret-table thead {
box-shadow: 0px 2px 2px var(--shadow-color);
position: sticky;
top: 0;
+ z-index: 1;
}
-table.pyret-table tr:last-child td:first-child {
- border-bottom-left-radius: var(--table-radius);
+/* top/bottom borders on the first and last rows */
+table.pyret-table th { border-top: solid 1px black; }
+table.pyret-table > :nth-last-child(1 of :has(tr)) > tr:last-child td {
+ border-bottom: solid 1px black;
}
-table.pyret-table tr:last-child td:last-child {
- border-bottom-right-radius: var(--table-radius);
+
+/* left/right borders on the first and last cells */
+table.pyret-table td:first-child, table.pyret-table th:first-child {
+ border-left: solid 1px black;
+}
+table.pyret-table td:last-child, table.pyret-table th:last-child {
+ border-right: solid 1px black;
}
-/* style first and last th elements to use the table's rounded corners */
-table.pyret-table th:first-child { border-top-left-radius: var(--table-radius); }
-table.pyret-table th:last-child { border-top-right-radius: var(--table-radius); }
+/* rounded borders for corners */
+table.pyret-table th:first-child {
+ border-top-left-radius: var(--table-radius);
+}
+table.pyret-table th:last-child {
+ border-top-right-radius: var(--table-radius);
+}
+table.pyret-table > :nth-last-child(1 of :has(tr)) > tr:last-child td:first-child {
+ border-bottom-left-radius: var(--table-radius);
+}
+table.pyret-table > :nth-last-child(1 of :has(tr)) > tr:last-child td:last-child {
+ border-bottom-right-radius: var(--table-radius);
+}
diff --git a/src/web/editor.html b/src/web/editor.html
index f8ebe386..fd59d9e7 100644
--- a/src/web/editor.html
+++ b/src/web/editor.html
@@ -445,6 +445,8 @@
Announcements
diff --git a/src/web/js/beforeBlocks.js b/src/web/js/beforeBlocks.js
index 58b5e756..f0973b0d 100644
--- a/src/web/js/beforeBlocks.js
+++ b/src/web/js/beforeBlocks.js
@@ -305,13 +305,13 @@ $(function() {
};
function setUsername(target) {
- return gwrap.load({name: 'plus',
+ return gwrap.load({name: 'people',
version: 'v1',
}).then((api) => {
- api.people.get({ userId: "me" }).then(function(user) {
- var name = user.displayName;
- if (user.emails && user.emails[0] && user.emails[0].value) {
- name = user.emails[0].value;
+ api.people.get({ resourceName: "people/me", personFields: "names,emailAddresses" }).then(function(user) {
+ var name = user.names && user.names[0] ? user.names[0].displayName : undefined;
+ if (user.emailAddresses && user.emailAddresses[0] && user.emailAddresses[0].value) {
+ name = user.emailAddresses[0].value;
}
target.text(name);
});
diff --git a/src/web/js/beforePyret.js b/src/web/js/beforePyret.js
index 38651f41..b4941603 100644
--- a/src/web/js/beforePyret.js
+++ b/src/web/js/beforePyret.js
@@ -3,6 +3,130 @@
var originalPageLoad = Date.now();
console.log("originalPageLoad: ", originalPageLoad);
+// Transparently route browser fetches to allowlisted hosts through the
+// server-side proxy at /load-shareurl, but only when the direct path doesn't
+// work.
+//
+// Strategy: the FIRST fetch to an allowlisted host fires direct + proxied in
+// parallel. We decide shouldProxy for the rest of the page-load from direct's
+// response *headers*:
+// - direct returned 2xx with content-type text/plain -> shouldProxy=false:
+// serve direct's response, abort the in-flight proxy fetch.
+// - direct failed, hung past timeout, or returned anything else
+// -> shouldProxy=true:
+// serve proxy's response.
+// A key idea is that network-blocky things sometimes return 200 with a
+// message page about blocking (or an error, but that counts as a fail). We
+// don't want to accidentally think that's a success.
+// shouldProxy state is in-memory and per-host — never persisted, since
+// reachability changes between networks and a stale value would silently
+// break loads.
+//
+// Installed on the global fetch as early as possible so it catches every fetch
+// caller; some of them are in the pyret-lang runtime and would be otherwise
+// difficult to configure.
+const SHAREURL_PROXY_HOSTS = new Set(['raw.githubusercontent.com']);
+const SHAREURL_DIRECT_TIMEOUT_MS = 5000;
+const _origFetch = window.fetch.bind(window);
+
+const _shareurlShouldProxy = new Map(); // host -> boolean
+const _shareurlShouldProxyInflight = new Map(); // host -> Promise
+
+function _shareurlProxyUrl(fetchInput) {
+ return '/load-shareurl?url=' + encodeURIComponent(_shareurlInputToUrl(fetchInput));
+}
+
+function _shareurlInputToUrl(fetchInput) {
+ return (typeof fetchInput === 'string') ? fetchInput
+ : (typeof Request !== 'undefined' && fetchInput instanceof Request) ? fetchInput.url
+ : String(fetchInput);
+}
+
+function _shareurlVerifyDirect(r) {
+ if (!r.ok) return false;
+ const ct = (r.headers.get('content-type') || '').toLowerCase();
+ // Source files served from raw.githubusercontent.com come back as
+ // text/plain (.arr, .json, .csv, .md all do). Anything else — HTML block
+ // pages, captive portals, surprise content types — we don't trust as a
+ // real upstream response.
+ return ct.startsWith('text/plain');
+}
+
+function _shareurlFetch(shouldProxy, fetchInput, fetchInit) {
+ const maybeProxyInput = shouldProxy ? _shareurlProxyUrl(fetchInput) : fetchInput;
+ return _origFetch(maybeProxyInput, fetchInit);
+}
+
+function _shareurlRace(fetchInput, fetchInit) {
+ const proxyCtrl = new AbortController();
+ // NOTE(joe): The signal overwrite is technically not the right fetch()
+ // polyfill. If the caller elsewhere in the codebase provided a different
+ // signal (which in the fetch API is only for aborting as of April '26), that
+ // caller aborting through that signal won't cancel the proxy fetch.
+ // I'm OK letting that case slip through here in exchange for not having a
+ // bunch of extra event handler forwarding
+ const proxyP = _origFetch(_shareurlProxyUrl(fetchInput),
+ Object.assign({}, fetchInit, { signal: proxyCtrl.signal }));
+ const directP = _origFetch(fetchInput, fetchInit).then(r => {
+ if (!_shareurlVerifyDirect(r)) throw new Error('direct request failed');
+ return r;
+ });
+
+ // shouldProxy: false iff direct verified before the timeout, else true.
+ // Whether to proxy is decided solely on whether direct succeeds or not
+ const shouldProxyPromise = Promise.race([
+ directP.then(() => false, () => true),
+ new Promise(resolve => setTimeout(() => resolve(true), SHAREURL_DIRECT_TIMEOUT_MS)),
+ ]);
+
+ // Settlement-order check: if direct verifies before proxy returns, abort
+ // the in-flight proxy to stop wasting server bandwidth. We must NOT
+ // abort once proxy has already returned, since by then the caller is
+ // reading proxy's body and aborting would error its stream mid-read.
+ const directFinishedSuccessfullyAndFirstP = Promise.race([
+ directP.then(() => true, () => false),
+ proxyP.then(() => false, () => false),
+ ]);
+ directFinishedSuccessfullyAndFirstP.then(directFirst => {
+ if (directFirst) proxyCtrl.abort();
+ });
+
+ // Caller's response: whichever of direct-verified or proxy fulfills
+ // first. If both fail, surface proxy's error (the more authoritative
+ // upstream — direct's may just be 'direct-not-verified').
+ const responsePromise = Promise.any([directP, proxyP]).catch(
+ aggErr => Promise.reject(aggErr.errors[1] || aggErr.errors[0])
+ );
+
+ return { responsePromise, shouldProxyPromise };
+}
+
+window.fetch = function(fetchInput, fetchInit) {
+ let host;
+ try { host = new URL(_shareurlInputToUrl(fetchInput), window.location.href).hostname; }
+ catch (_) { return _origFetch(fetchInput, fetchInit); }
+ if (!SHAREURL_PROXY_HOSTS.has(host)) return _origFetch(fetchInput, fetchInit);
+
+ const shouldProxy = _shareurlShouldProxy.get(host);
+ const inflight = _shareurlShouldProxyInflight.get(host);
+ if (shouldProxy !== undefined) {
+ return _shareurlFetch(shouldProxy, fetchInput, fetchInit);
+ } else if (inflight) {
+ // shouldProxy pending: queue this fetch on it and issue a single fresh
+ // request once shouldProxy is decided.
+ return inflight.then(sp => _shareurlFetch(sp, fetchInput, fetchInit));
+ } else {
+ // First fetch to this host this page-load: run the race.
+ const { responsePromise, shouldProxyPromise } = _shareurlRace(fetchInput, fetchInit);
+ _shareurlShouldProxyInflight.set(host, shouldProxyPromise);
+ shouldProxyPromise.then(sp => {
+ _shareurlShouldProxy.set(host, sp);
+ _shareurlShouldProxyInflight.delete(host);
+ });
+ return responsePromise;
+ }
+};
+
const isEmbedded = window.parent !== window;
var shareAPI = makeShareAPI(process.env.CURRENT_PYRET_RELEASE);
@@ -333,13 +457,13 @@ $(function() {
};
function setUsername(target) {
- return gwrap.load({name: 'plus',
+ return gwrap.load({name: 'people',
version: 'v1',
}).then((api) => {
- api.people.get({ userId: "me" }).then(function(user) {
- var name = user.displayName;
- if (user.emails && user.emails[0] && user.emails[0].value) {
- name = user.emails[0].value;
+ api.people.get({ resourceName: "people/me", personFields: "names,emailAddresses" }).then(function(user) {
+ var name = user.names && user.names[0] ? user.names[0].displayName : undefined;
+ if (user.emailAddresses && user.emailAddresses[0] && user.emailAddresses[0].value) {
+ name = user.emailAddresses[0].value;
}
target.text(name);
});
diff --git a/src/web/js/cpo-ide-hooks.js b/src/web/js/cpo-ide-hooks.js
index c5406ee0..935cb6c9 100644
--- a/src/web/js/cpo-ide-hooks.js
+++ b/src/web/js/cpo-ide-hooks.js
@@ -32,13 +32,12 @@
],
nativeRequires: [
"cpo/cpo-builtin-modules",
- "pyret-base/js/js-numbers",
],
provides: {},
theModule: function(runtime, namespace, uri,
compileLib, compileStructs, pyRepl, cpo,
runtimeLib, loadLib, cpoBuiltins, parsePyret,
- cpoModules, jsnums
+ cpoModules
) {
window.CPOIDEHooks = {
runtime: runtime,
@@ -49,7 +48,7 @@
compileStructs: compileStructs,
compileLib: compileLib,
cpoModules: cpoModules,
- jsnums: jsnums
+ jsnums: runtime.jsnums
};
return runtime.makeModuleReturn({}, {});
}
diff --git a/src/web/js/dashboard/GoogleAPI.js b/src/web/js/dashboard/GoogleAPI.js
index 22571472..6fe442b8 100644
--- a/src/web/js/dashboard/GoogleAPI.js
+++ b/src/web/js/dashboard/GoogleAPI.js
@@ -151,11 +151,11 @@ class GoogleAPI {
}
getUsername = () => {
- return gwrap.load({name: 'plus',
+ return gwrap.load({name: 'people',
version: 'v1',
}).then((api) => {
console.log("Api: ", api);
- return api.people.get({ userId: "me" });
+ return api.people.get({ resourceName: "people/me", personFields: "names,emailAddresses" });
});
}
}
diff --git a/src/web/js/dashboard/StudentDashboard.js b/src/web/js/dashboard/StudentDashboard.js
index b64edeb7..cf3f46aa 100644
--- a/src/web/js/dashboard/StudentDashboard.js
+++ b/src/web/js/dashboard/StudentDashboard.js
@@ -31,7 +31,7 @@ class StudentDashboard extends Component {
this.setState({signedIn: SIGNED_IN});
this.updateRecentFiles();
this.api.getUsername().then((userInfo) => {
- this.setState({ userName: userInfo.emails[0].value });
+ this.setState({ userName: userInfo.emailAddresses[0].value });
});
}
else {
@@ -52,7 +52,7 @@ class StudentDashboard extends Component {
this.api.signIn().then((resp) => {
this.setState({signedIn: SIGNED_IN});
this.api.getUsername().then((userInfo) => {
- this.setState({ userName: userInfo.emails[0].value });
+ this.setState({ userName: userInfo.emailAddresses[0].value });
});
this.updateRecentFiles();
})
diff --git a/src/web/js/google-apis/api-wrapper.js b/src/web/js/google-apis/api-wrapper.js
index 7cb72cf1..2d2308d1 100644
--- a/src/web/js/google-apis/api-wrapper.js
+++ b/src/web/js/google-apis/api-wrapper.js
@@ -394,10 +394,23 @@ function loadAPIWrapper(immediate) {
}
function processDelta() {
+ debugger;
var newKeys = Object.keys(gapi.client)
.filter(function(k) {return (preKeys.indexOf(k) === -1);});
var ret;
- if (newKeys.length > 1) {
+ if (params.name && newKeys.indexOf(params.name) !== -1) {
+ // When we requested a specific API by name, return only that one.
+ // Other new keys (from concurrent loads) will be picked up by
+ // their own loadAPI calls. Process all keys to populate the cache,
+ // but only return the one we asked for.
+ newKeys.forEach(processKey);
+ ret = _GWRAP_APIS[params.name];
+ } else if (newKeys.length > 1) {
+ // NOTE(joe, Apr 2026): We added this to diagnose if it actually ever
+ // happens in prod that we return a list of new APIs.
+ // We think it probably doesn't based on call sites, but don't want to
+ // break anything.
+ logger.log('processdelta-multi-key-return', { newKeys, preKeys });
ret = newKeys.map(processKey);
} else if (params.name && newKeys.length === 0) {
// Hack to make drive-loading happy on login
diff --git a/src/web/js/log.js b/src/web/js/log.js
index cafcbe33..35619eb5 100644
--- a/src/web/js/log.js
+++ b/src/web/js/log.js
@@ -10,6 +10,25 @@ var DummyBackend = function () {
this.log = function(_, __){};
};
+var FetchWithCredsBackend = function (url, user, pass) {
+ const auth = btoa(user + ":" + pass);
+ this.log = function(name, obj) {
+ const opts = {
+ method: 'POST',
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ 'Authorization': 'Basic ' + auth
+ },
+ body: JSON.stringify(obj)
+ };
+ const req = fetch("https://bootstrapworld.org/data/actions/LogActions.php?method=pyretLog", opts).then((resp) => {
+ console.log(resp);
+ }).catch((e) => console.error(e));
+ };
+
+}
+
var AJAXBackend = function (url) {
this.log = function (name, obj) {
var request = new XMLHttpRequest();
@@ -18,6 +37,17 @@ var AJAXBackend = function (url) {
}
}
+var backend;
+if(window.LOG_URL && window.LOG_USER) {
+ backend = new FetchWithCredsBackend(LOG_URL, LOG_USER, LOG_PASSWORD);
+}
+else if(window.LOG_URL) {
+ backend = new AJAXBackend(LOG_URL);
+}
+else {
+ backend = new DummyBackend();
+}
+
var logger = (function(backend) {
var sessionStorage;
var localStorage;
@@ -100,7 +130,7 @@ var logger = (function(backend) {
return nowIsDetailed;
}
};
-})( LOG_URL ? new AJAXBackend(LOG_URL) : new DummyBackend() );
+})(backend);
if(window.CodeMirror) {
CodeMirror.defineOption('logging', false,
diff --git a/src/web/js/output-ui.js b/src/web/js/output-ui.js
index acba54a8..8dfbbe96 100644
--- a/src/web/js/output-ui.js
+++ b/src/web/js/output-ui.js
@@ -16,10 +16,10 @@
provides: {},
nativeRequires: [
"pyret-base/js/runtime-util",
- "pyret-base/js/js-numbers"
],
- theModule: function(runtime, _, uri, parsePyret, errordisplayLib, srclocLib, astLib, imageLib, loadLib, util, jsnums) {
+ theModule: function(runtime, _, uri, parsePyret, errordisplayLib, srclocLib, astLib, imageLib, loadLib, util) {
+ var jsnums = runtime.jsnums;
var image = runtime.getField(imageLib, "internal");
var srcloc = runtime.getField(srclocLib, "values");
var isSrcloc = runtime.getField(srcloc, "is-Srcloc");
@@ -1399,7 +1399,7 @@
// after, and numerals to be repeated.
var numr = num.numerator();
var denr = num.denominator();
- var decimal = jsnums.toRepeatingDecimal(numr, denr, runtime.NumberErrbacks);
+ var decimal = jsnums.toRepeatingDecimal(numr, denr);
var prePointString = decimal[0];
var postPointString = decimal[1];
var decRpt = decimal[2];
diff --git a/src/web/js/trove/chart-lib.js b/src/web/js/trove/chart-lib.js
index 95461502..e18f32e7 100644
--- a/src/web/js/trove/chart-lib.js
+++ b/src/web/js/trove/chart-lib.js
@@ -4,7 +4,6 @@
{ 'import-type': 'builtin', 'name': 'image-lib' },
],
nativeRequires: [
- 'pyret-base/js/js-numbers',
'google-charts',
],
provides: {
@@ -17,8 +16,9 @@
'plot': "tany",
}
},
- theModule: function (RUNTIME, NAMESPACE, uri, IMAGELIB, jsnums , google) {
- 'use strict';
+ theModule: function (RUNTIME, NAMESPACE, uri, IMAGELIB, google) {
+ 'use strict';
+ var jsnums = RUNTIME.jsnums;
// Load google library via editor.html to avoid loading issues
function notImp(name) {
@@ -89,7 +89,7 @@
function getPrettyNumToStringDigits(d) {
// this accepts Pyret num
return n =>
- jsnums.toStringDigits(n, d, RUNTIME.NumberErrbacks).replace(/\.?0*$/, '');
+ jsnums.toStringDigits(n, d).replace(/\.?0*$/, '');
}
const prettyNumToStringDigits5 = getPrettyNumToStringDigits(5);
@@ -180,8 +180,7 @@
xMaxC.removeClass('error-bg');
xMaxC.addClass('ok-bg');
- if (jsnums.greaterThanOrEqual(xMinVal, xMaxVal,
- RUNTIME.NumberErrbacks)) {
+ if (jsnums.greaterThanOrEqual(xMinVal, xMaxVal)) {
xMinC.addClass('error-bg');
xMaxC.addClass('error-bg');
xMinC.removeClass('ok-bg');
@@ -211,8 +210,7 @@
yMaxC.removeClass('error-bg');
yMaxC.addClass('ok-bg');
- if (jsnums.greaterThanOrEqual(xMinVal, xMaxVal,
- RUNTIME.NumberErrbacks)) {
+ if (jsnums.greaterThanOrEqual(xMinVal, xMaxVal)) {
yMinC.addClass('error-bg');
yMaxC.addClass('error-bg');
yMinC.removeClass('ok-bg');
@@ -232,8 +230,7 @@
numSamplesC.addClass('ok-bg');
if (!isTrue(RUNTIME.num_is_integer(numSamplesVal)) ||
- jsnums.lessThanOrEqual(numSamplesVal, 1,
- RUNTIME.NumberErrbacks)) {
+ jsnums.lessThanOrEqual(numSamplesVal, 1)) {
numSamplesC.addClass('error-bg');
numSamplesC.removeClass('ok-bg');
return null;
diff --git a/src/web/js/trove/d3-lib-list.js b/src/web/js/trove/d3-lib-list.js
index 46c88065..d74b1817 100644
--- a/src/web/js/trove/d3-lib-list.js
+++ b/src/web/js/trove/d3-lib-list.js
@@ -3,13 +3,13 @@
{ 'import-type': 'builtin', 'name': 'image-lib' },
],
nativeRequires: [
- 'pyret-base/js/js-numbers',
'd3'
],
provides: {},
- theModule: function (RUNTIME, NAMESPACE, uri, IMAGELIB, jsnums, d3) {
+ theModule: function (RUNTIME, NAMESPACE, uri, IMAGELIB, d3) {
'use strict';
+ var jsnums = RUNTIME.jsnums;
var IMAGE = RUNTIME.getField(IMAGELIB, "internal");
@@ -34,13 +34,13 @@
* @return {jsnums -> jsnums}
*/
return function (k) {
- var oldDiff = jsnums.subtract(k, oldX, RUNTIME.NumberErrbacks);
- var oldRange = jsnums.subtract(oldY, oldX, RUNTIME.NumberErrbacks);
- var portion = jsnums.divide(oldDiff, oldRange, RUNTIME.NumberErrbacks);
- var newRange = jsnums.subtract(newY, newX, RUNTIME.NumberErrbacks);
- var newPortion = jsnums.multiply(portion, newRange, RUNTIME.NumberErrbacks);
- var result = jsnums.add(newPortion, newX, RUNTIME.NumberErrbacks);
- return toFixnum ? jsnums.toFixnum(result, RUNTIME.NumberErrbacks) : result;
+ var oldDiff = jsnums.subtract(k, oldX);
+ var oldRange = jsnums.subtract(oldY, oldX);
+ var portion = jsnums.divide(oldDiff, oldRange);
+ var newRange = jsnums.subtract(newY, newX);
+ var newPortion = jsnums.multiply(portion, newRange);
+ var result = jsnums.add(newPortion, newX);
+ return toFixnum ? jsnums.toFixnum(result) : result;
};
}
@@ -50,13 +50,13 @@
function getPrettyNumToStringDigits(digits) {
return function (num) {
- return jsnums.toStringDigits(num, digits, RUNTIME.NumberErrbacks).replace(/\.?0*$/, '');
+ return jsnums.toStringDigits(num, digits).replace(/\.?0*$/, '');
};
}
function between(b, a, c) {
- return (jsnums.lessThanOrEqual(a, b, RUNTIME.NumberErrbacks) && jsnums.lessThanOrEqual(b, c, RUNTIME.NumberErrbacks)) ||
- (jsnums.lessThanOrEqual(c, b, RUNTIME.NumberErrbacks) && jsnums.lessThanOrEqual(b, a, RUNTIME.NumberErrbacks));
+ return (jsnums.lessThanOrEqual(a, b) && jsnums.lessThanOrEqual(b, c)) ||
+ (jsnums.lessThanOrEqual(c, b) && jsnums.lessThanOrEqual(b, a));
}
function numMin(a, b) { /* ignore the rest */
diff --git a/src/web/js/trove/d3-lib.js b/src/web/js/trove/d3-lib.js
index 421bdd09..73b85596 100644
--- a/src/web/js/trove/d3-lib.js
+++ b/src/web/js/trove/d3-lib.js
@@ -3,13 +3,13 @@
{ 'import-type': 'builtin', 'name': 'image-lib' },
],
nativeRequires: [
- 'pyret-base/js/js-numbers',
'd3'
],
provides: {},
- theModule: function (RUNTIME, NAMESPACE, uri, IMAGELIB, jsnums, d3) {
+ theModule: function (RUNTIME, NAMESPACE, uri, IMAGELIB, d3) {
'use strict';
+ var jsnums = RUNTIME.jsnums;
var IMAGE = RUNTIME.getField(IMAGELIB, "internal");
function assert(val, msg) {
@@ -33,13 +33,13 @@
* @return {jsnums -> jsnums}
*/
return function (k) {
- var oldDiff = jsnums.subtract(k, oldX, RUNTIME.NumberErrbacks);
- var oldRange = jsnums.subtract(oldY, oldX, RUNTIME.NumberErrbacks);
- var portion = jsnums.divide(oldDiff, oldRange, RUNTIME.NumberErrbacks);
- var newRange = jsnums.subtract(newY, newX, RUNTIME.NumberErrbacks);
- var newPortion = jsnums.multiply(portion, newRange, RUNTIME.NumberErrbacks);
- var result = jsnums.add(newPortion, newX, RUNTIME.NumberErrbacks);
- return toFixnum ? jsnums.toFixnum(result, RUNTIME.NumberErrbacks) : result;
+ var oldDiff = jsnums.subtract(k, oldX);
+ var oldRange = jsnums.subtract(oldY, oldX);
+ var portion = jsnums.divide(oldDiff, oldRange);
+ var newRange = jsnums.subtract(newY, newX);
+ var newPortion = jsnums.multiply(portion, newRange);
+ var result = jsnums.add(newPortion, newX);
+ return toFixnum ? jsnums.toFixnum(result) : result;
};
}
@@ -49,13 +49,13 @@
function getPrettyNumToStringDigits(digits) {
return function (num) {
- return jsnums.toStringDigits(num, digits, RUNTIME.NumberErrbacks).replace(/\.?0*$/, '');
+ return jsnums.toStringDigits(num, digits).replace(/\.?0*$/, '');
};
}
function between(b, a, c) {
- return (jsnums.lessThanOrEqual(a, b, RUNTIME.NumberErrbacks) && jsnums.lessThanOrEqual(b, c, RUNTIME.NumberErrbacks)) ||
- (jsnums.lessThanOrEqual(c, b, RUNTIME.NumberErrbacks) && jsnums.lessThanOrEqual(b, a, RUNTIME.NumberErrbacks));
+ return (jsnums.lessThanOrEqual(a, b) && jsnums.lessThanOrEqual(b, c)) ||
+ (jsnums.lessThanOrEqual(c, b) && jsnums.lessThanOrEqual(b, a));
}
function numMin(a, b) { /* ignore the rest */
diff --git a/src/web/js/trove/plot-lib-list.js b/src/web/js/trove/plot-lib-list.js
index c285c11a..05672e0f 100644
--- a/src/web/js/trove/plot-lib-list.js
+++ b/src/web/js/trove/plot-lib-list.js
@@ -3,7 +3,6 @@
{ 'import-type': 'builtin', 'name': 'd3-lib-list' },
],
nativeRequires: [
- 'pyret-base/js/js-numbers',
'd3',
'd3-tip'
],
@@ -17,8 +16,9 @@
'box-chart': "tany"
}
},
- theModule: function (RUNTIME, NAMESPACE, uri, CLIB, jsnums, d3, D3TIP) {
- 'use strict';
+ theModule: function (RUNTIME, NAMESPACE, uri, CLIB, d3, D3TIP) {
+ 'use strict';
+ var jsnums = RUNTIME.jsnums;
var gf = RUNTIME.getField,
cases = RUNTIME.ffi.cases;
var libNum = CLIB.libNum,
@@ -69,7 +69,7 @@
function getAxisConf(aMin, aMax) {
const conf = {},
scaler = libNum.scaler(aMin, aMax, 0, 1, false),
- pos = jsnums.toFixnum(scaler(0), RUNTIME.NumberErrbacks);
+ pos = jsnums.toFixnum(scaler(0));
if (0 <= pos && pos <= 1) {
conf.bold = true;
@@ -285,7 +285,7 @@
xMaxC.removeClass('error-bg');
xMaxC.addClass('ok-bg');
- if (jsnums.greaterThanOrEqual(xMin_val, xMax_val, RUNTIME.NumberErrbacks)) {
+ if (jsnums.greaterThanOrEqual(xMin_val, xMax_val)) {
xMinC.addClass('error-bg');
xMaxC.addClass('error-bg');
xMinC.removeClass('ok-bg');
@@ -313,7 +313,7 @@
yMaxC.removeClass('error-bg');
yMaxC.addClass('ok-bg');
- if (jsnums.greaterThanOrEqual(xMin_val, xMax_val, RUNTIME.NumberErrbacks)) {
+ if (jsnums.greaterThanOrEqual(xMin_val, xMax_val)) {
yMinC.addClass('error-bg');
yMaxC.addClass('error-bg');
yMinC.removeClass('ok-bg');
@@ -332,7 +332,7 @@
numSamplesC.addClass('ok-bg');
if (RUNTIME.isPyretFalse(RUNTIME.num_is_integer(numSamples_val)) ||
- jsnums.lessThanOrEqual(numSamples_val, 1, RUNTIME.NumberErrbacks)) {
+ jsnums.lessThanOrEqual(numSamples_val, 1)) {
numSamplesC.addClass('error-bg');
numSamplesC.removeClass('ok-bg');
return null;
@@ -372,9 +372,9 @@
if (newWindow === null) { return; }
var xMin_val = newWindow['_x-min'];
var xMax_val = newWindow['_x-max'];
- var move = jsnums.divide(jsnums.subtract(xMax_val, xMin_val, RUNTIME.NumberErrbacks), 10, RUNTIME.NumberErrbacks);
- xMinC.val(prettyNumToStringDigits20(jsnums.subtract(xMin_val, move, RUNTIME.NumberErrbacks)));
- xMaxC.val(prettyNumToStringDigits20(jsnums.subtract(xMax_val, move, RUNTIME.NumberErrbacks)));
+ var move = jsnums.divide(jsnums.subtract(xMax_val, xMin_val), 10);
+ xMinC.val(prettyNumToStringDigits20(jsnums.subtract(xMin_val, move)));
+ xMaxC.val(prettyNumToStringDigits20(jsnums.subtract(xMax_val, move)));
}));
controller.append($('', {
text: '⇨',
@@ -388,8 +388,8 @@
var xMin_val = newWindow['_x-min'];
var xMax_val = newWindow['_x-max'];
var move = jsnums.divide(jsnums.subtract(xMax_val, xMin_val), 10);
- xMinC.val(prettyNumToStringDigits20(jsnums.add(xMin_val, move, RUNTIME.NumberErrbacks)));
- xMaxC.val(prettyNumToStringDigits20(jsnums.add(xMax_val, move, RUNTIME.NumberErrbacks)));
+ xMinC.val(prettyNumToStringDigits20(jsnums.add(xMin_val, move)));
+ xMaxC.val(prettyNumToStringDigits20(jsnums.add(xMax_val, move)));
}));
controller.append($('', {
text: '⇩',
@@ -403,8 +403,8 @@
var yMin_val = newWindow['_y-min'];
var yMax_val = newWindow['_y-max'];
var move = jsnums.divide(jsnums.subtract(yMax_val, yMin_val), 10);
- yMinC.val(prettyNumToStringDigits20(jsnums.subtract(yMin_val, move, RUNTIME.NumberErrbacks)));
- yMaxC.val(prettyNumToStringDigits20(jsnums.subtract(yMax_val, move, RUNTIME.NumberErrbacks)));
+ yMinC.val(prettyNumToStringDigits20(jsnums.subtract(yMin_val, move)));
+ yMaxC.val(prettyNumToStringDigits20(jsnums.subtract(yMax_val, move)));
}));
controller.append($('', {
text: '⇧',
@@ -418,8 +418,8 @@
var yMin_val = newWindow['_y-min'];
var yMax_val = newWindow['_y-max'];
var move = jsnums.divide(jsnums.subtract(yMax_val, yMin_val), 10);
- yMinC.val(prettyNumToStringDigits20(jsnums.add(yMin_val, move, RUNTIME.NumberErrbacks)));
- yMaxC.val(prettyNumToStringDigits20(jsnums.add(yMax_val, move, RUNTIME.NumberErrbacks)));
+ yMinC.val(prettyNumToStringDigits20(jsnums.add(yMin_val, move)));
+ yMaxC.val(prettyNumToStringDigits20(jsnums.add(yMax_val, move)));
}));
var redraw = $('', {
@@ -469,10 +469,10 @@
var cy = pixelToY(coord[1]);
var radiusY = jsnums.subtract(yMax, yMin);
- xMinC.val(prettyNumToStringDigits20(jsnums.subtract(cx, radiusX, RUNTIME.NumberErrbacks)));
- xMaxC.val(prettyNumToStringDigits20(jsnums.add(cx, radiusX, RUNTIME.NumberErrbacks)));
- yMinC.val(prettyNumToStringDigits20(jsnums.subtract(cy, radiusY, RUNTIME.NumberErrbacks)));
- yMaxC.val(prettyNumToStringDigits20(jsnums.add(cy, radiusY, RUNTIME.NumberErrbacks)));
+ xMinC.val(prettyNumToStringDigits20(jsnums.subtract(cx, radiusX)));
+ xMaxC.val(prettyNumToStringDigits20(jsnums.add(cx, radiusX)));
+ yMinC.val(prettyNumToStringDigits20(jsnums.subtract(cy, radiusY)));
+ yMaxC.val(prettyNumToStringDigits20(jsnums.add(cy, radiusY)));
})
.on('mousedown', function () {
@@ -619,16 +619,16 @@
var yMax = gf(windowOptions, '_y-max');
function inBound(p) {
- return jsnums.lessThanOrEqual(xMin, p[0], RUNTIME.NumberErrbacks) &&
- jsnums.lessThanOrEqual(p[0], xMax, RUNTIME.NumberErrbacks) &&
- jsnums.lessThanOrEqual(yMin, p[1], RUNTIME.NumberErrbacks) &&
- jsnums.lessThanOrEqual(p[1], yMax, RUNTIME.NumberErrbacks);
+ return jsnums.lessThanOrEqual(xMin, p[0]) &&
+ jsnums.lessThanOrEqual(p[0], xMax) &&
+ jsnums.lessThanOrEqual(yMin, p[1]) &&
+ jsnums.lessThanOrEqual(p[1], yMax);
}
function dist(a, b) {
return jsnums.add(
- jsnums.sqr(jsnums.subtract(a[0], b[0], RUNTIME.NumberErrbacks)),
- jsnums.sqr(jsnums.subtract(a[1], b[1], RUNTIME.NumberErrbacks)), RUNTIME.NumberErrbacks);
+ jsnums.sqr(jsnums.subtract(a[0], b[0])),
+ jsnums.sqr(jsnums.subtract(a[1], b[1])));
}
function nearest(candidates, origin) {
@@ -636,7 +636,7 @@
var optimal = null;
candidates.forEach(function (candidate) {
var distance = dist(candidate, origin);
- if (optimal === null || jsnums.lessThan(distance, optimal, RUNTIME.NumberErrbacks)) {
+ if (optimal === null || jsnums.lessThan(distance, optimal)) {
optimal = distance;
ans = candidate;
}
@@ -645,7 +645,7 @@
}
function equal(a, b) {
- return jsnums.lessThanOrEqual(a, b, RUNTIME.NumberErrbacks) && jsnums.lessThanOrEqual(b, a, RUNTIME.NumberErrbacks);
+ return jsnums.lessThanOrEqual(a, b) && jsnums.lessThanOrEqual(b, a);
}
function findPointOnEdge(near, far) {
@@ -677,15 +677,15 @@
x = (y - c) / m [4] [rewrite 3]
*/
- var m = jsnums.divide(jsnums.subtract(near[1], far[1], RUNTIME.NumberErrbacks), jsnums.subtract(near[0], far[0], RUNTIME.NumberErrbacks));
- var c = jsnums.subtract(near[1], jsnums.multiply(m, near[0], RUNTIME.NumberErrbacks), RUNTIME.NumberErrbacks);
+ var m = jsnums.divide(jsnums.subtract(near[1], far[1]), jsnums.subtract(near[0], far[0]));
+ var c = jsnums.subtract(near[1], jsnums.multiply(m, near[0]));
var f = function (x) {
- return jsnums.add(jsnums.multiply(m, x, RUNTIME.NumberErrbacks), c, RUNTIME.NumberErrbacks);
+ return jsnums.add(jsnums.multiply(m, x), c);
};
var g = function (y) {
- return jsnums.divide(jsnums.subtract(y, c, RUNTIME.NumberErrbacks), m, RUNTIME.NumberErrbacks);
+ return jsnums.divide(jsnums.subtract(y, c), m);
};
candidates = [
@@ -702,10 +702,10 @@
}
return nearest(candidates.filter(function (p) {
- return jsnums.lessThanOrEqual(pxMin, p[0], RUNTIME.NumberErrbacks) &&
- jsnums.lessThanOrEqual(p[0], pxMax, RUNTIME.NumberErrbacks) &&
- jsnums.lessThanOrEqual(pyMin, p[1], RUNTIME.NumberErrbacks) &&
- jsnums.lessThanOrEqual(p[1], pyMax, RUNTIME.NumberErrbacks);
+ return jsnums.lessThanOrEqual(pxMin, p[0]) &&
+ jsnums.lessThanOrEqual(p[0], pxMax) &&
+ jsnums.lessThanOrEqual(pyMin, p[1]) &&
+ jsnums.lessThanOrEqual(p[1], pyMax);
}), near);
}
@@ -716,8 +716,8 @@
function toJSOptions(options) {
return {
color: CLIB.libColor.convertColor(gf(options, 'color')),
- size: jsnums.toFixnum(gf(options, 'size'), RUNTIME.NumberErrbacks),
- opacity: jsnums.toFixnum(gf(options, 'opacity'), RUNTIME.NumberErrbacks),
+ size: jsnums.toFixnum(gf(options, 'size')),
+ opacity: jsnums.toFixnum(gf(options, 'opacity')),
tip: RUNTIME.isPyretTrue(gf(options, 'tip')),
};
}
@@ -913,7 +913,7 @@
var sum = tab.map(function (row) { return row[1]; })
.reduce(function (a, b) {
- return jsnums.add(a, b, RUNTIME.NumberErrbacks);
+ return jsnums.add(a, b);
});
var valueScaler = libNum.scaler(0, sum, 0, 100, true);
@@ -1084,7 +1084,7 @@
.scale(y)
.orient('left')
.tickFormat(function (d) {
- return prettyNumToStringDigitsForAxis(yAxisDisplayScaler(jsnums.fromFixnum(d, RUNTIME.NumberErrbacks)));
+ return prettyNumToStringDigitsForAxis(yAxisDisplayScaler(jsnums.fromFixnum(d)));
});
x0.domain(data.map(function (d) { return d.label; }));
@@ -1352,7 +1352,7 @@
.scale(y)
.orient('left')
.tickFormat(function (d) {
- return prettyNumToStringDigitsForAxis(yAxisDisplayScaler(jsnums.fromFixnum(d, RUNTIME.NumberErrbacks)));
+ return prettyNumToStringDigitsForAxis(yAxisDisplayScaler(jsnums.fromFixnum(d)));
});
x0.domain(data.map(function (d) { return d.label; }));
diff --git a/src/web/js/trove/plot-lib.js b/src/web/js/trove/plot-lib.js
index 458168d3..57659e96 100644
--- a/src/web/js/trove/plot-lib.js
+++ b/src/web/js/trove/plot-lib.js
@@ -3,7 +3,6 @@
{ 'import-type': 'builtin', 'name': 'd3-lib' },
],
nativeRequires: [
- 'pyret-base/js/js-numbers',
'd3',
'd3-tip'
],
@@ -17,8 +16,9 @@
'box-chart': "tany"
}
},
- theModule: function (RUNTIME, NAMESPACE, uri, CLIB, jsnums, d3, D3TIP) {
- 'use strict';
+ theModule: function (RUNTIME, NAMESPACE, uri, CLIB, d3, D3TIP) {
+ 'use strict';
+ var jsnums = RUNTIME.jsnums;
var gf = RUNTIME.getField,
cases = RUNTIME.ffi.cases;
var libNum = CLIB.libNum,
@@ -69,7 +69,7 @@
function getAxisConf(aMin, aMax) {
const conf = {},
scaler = libNum.scaler(aMin, aMax, 0, 1, false),
- pos = jsnums.toFixnum(scaler(0), RUNTIME.NumberErrbacks);
+ pos = jsnums.toFixnum(scaler(0));
if (0 <= pos && pos <= 1) {
conf.bold = true;
@@ -285,7 +285,7 @@
xMaxC.removeClass('error-bg');
xMaxC.addClass('ok-bg');
- if (jsnums.greaterThanOrEqual(xMin_val, xMax_val, RUNTIME.NumberErrbacks)) {
+ if (jsnums.greaterThanOrEqual(xMin_val, xMax_val)) {
xMinC.addClass('error-bg');
xMaxC.addClass('error-bg');
xMinC.removeClass('ok-bg');
@@ -313,7 +313,7 @@
yMaxC.removeClass('error-bg');
yMaxC.addClass('ok-bg');
- if (jsnums.greaterThanOrEqual(xMin_val, xMax_val, RUNTIME.NumberErrbacks)) {
+ if (jsnums.greaterThanOrEqual(xMin_val, xMax_val)) {
yMinC.addClass('error-bg');
yMaxC.addClass('error-bg');
yMinC.removeClass('ok-bg');
@@ -332,7 +332,7 @@
numSamplesC.addClass('ok-bg');
if (RUNTIME.isPyretFalse(RUNTIME.num_is_integer(numSamples_val)) ||
- jsnums.lessThanOrEqual(numSamples_val, 1, RUNTIME.NumberErrbacks)) {
+ jsnums.lessThanOrEqual(numSamples_val, 1)) {
numSamplesC.addClass('error-bg');
numSamplesC.removeClass('ok-bg');
return null;
@@ -372,9 +372,9 @@
if (newWindow === null) { return; }
var xMin_val = newWindow['x-min'];
var xMax_val = newWindow['x-max'];
- var move = jsnums.divide(jsnums.subtract(xMax_val, xMin_val, RUNTIME.NumberErrbacks), 10, RUNTIME.NumberErrbacks);
- xMinC.val(prettyNumToStringDigits20(jsnums.subtract(xMin_val, move, RUNTIME.NumberErrbacks)));
- xMaxC.val(prettyNumToStringDigits20(jsnums.subtract(xMax_val, move, RUNTIME.NumberErrbacks)));
+ var move = jsnums.divide(jsnums.subtract(xMax_val, xMin_val), 10);
+ xMinC.val(prettyNumToStringDigits20(jsnums.subtract(xMin_val, move)));
+ xMaxC.val(prettyNumToStringDigits20(jsnums.subtract(xMax_val, move)));
}));
controller.append($('', {
text: '⇨',
@@ -388,8 +388,8 @@
var xMin_val = newWindow['x-min'];
var xMax_val = newWindow['x-max'];
var move = jsnums.divide(jsnums.subtract(xMax_val, xMin_val), 10);
- xMinC.val(prettyNumToStringDigits20(jsnums.add(xMin_val, move, RUNTIME.NumberErrbacks)));
- xMaxC.val(prettyNumToStringDigits20(jsnums.add(xMax_val, move, RUNTIME.NumberErrbacks)));
+ xMinC.val(prettyNumToStringDigits20(jsnums.add(xMin_val, move)));
+ xMaxC.val(prettyNumToStringDigits20(jsnums.add(xMax_val, move)));
}));
controller.append($('', {
text: '⇩',
@@ -403,8 +403,8 @@
var yMin_val = newWindow['y-min'];
var yMax_val = newWindow['y-max'];
var move = jsnums.divide(jsnums.subtract(yMax_val, yMin_val), 10);
- yMinC.val(prettyNumToStringDigits20(jsnums.subtract(yMin_val, move, RUNTIME.NumberErrbacks)));
- yMaxC.val(prettyNumToStringDigits20(jsnums.subtract(yMax_val, move, RUNTIME.NumberErrbacks)));
+ yMinC.val(prettyNumToStringDigits20(jsnums.subtract(yMin_val, move)));
+ yMaxC.val(prettyNumToStringDigits20(jsnums.subtract(yMax_val, move)));
}));
controller.append($('', {
text: '⇧',
@@ -418,8 +418,8 @@
var yMin_val = newWindow['y-min'];
var yMax_val = newWindow['y-max'];
var move = jsnums.divide(jsnums.subtract(yMax_val, yMin_val), 10);
- yMinC.val(prettyNumToStringDigits20(jsnums.add(yMin_val, move, RUNTIME.NumberErrbacks)));
- yMaxC.val(prettyNumToStringDigits20(jsnums.add(yMax_val, move, RUNTIME.NumberErrbacks)));
+ yMinC.val(prettyNumToStringDigits20(jsnums.add(yMin_val, move)));
+ yMaxC.val(prettyNumToStringDigits20(jsnums.add(yMax_val, move)));
}));
var redraw = $('', {
@@ -469,10 +469,10 @@
var cy = pixelToY(coord[1]);
var radiusY = jsnums.subtract(yMax, yMin);
- xMinC.val(prettyNumToStringDigits20(jsnums.subtract(cx, radiusX, RUNTIME.NumberErrbacks)));
- xMaxC.val(prettyNumToStringDigits20(jsnums.add(cx, radiusX, RUNTIME.NumberErrbacks)));
- yMinC.val(prettyNumToStringDigits20(jsnums.subtract(cy, radiusY, RUNTIME.NumberErrbacks)));
- yMaxC.val(prettyNumToStringDigits20(jsnums.add(cy, radiusY, RUNTIME.NumberErrbacks)));
+ xMinC.val(prettyNumToStringDigits20(jsnums.subtract(cx, radiusX)));
+ xMaxC.val(prettyNumToStringDigits20(jsnums.add(cx, radiusX)));
+ yMinC.val(prettyNumToStringDigits20(jsnums.subtract(cy, radiusY)));
+ yMaxC.val(prettyNumToStringDigits20(jsnums.add(cy, radiusY)));
})
.on('mousedown', function () {
@@ -619,16 +619,16 @@
var yMax = gf(windowOptions, 'y-max');
function inBound(p) {
- return jsnums.lessThanOrEqual(xMin, p[0], RUNTIME.NumberErrbacks) &&
- jsnums.lessThanOrEqual(p[0], xMax, RUNTIME.NumberErrbacks) &&
- jsnums.lessThanOrEqual(yMin, p[1], RUNTIME.NumberErrbacks) &&
- jsnums.lessThanOrEqual(p[1], yMax, RUNTIME.NumberErrbacks);
+ return jsnums.lessThanOrEqual(xMin, p[0]) &&
+ jsnums.lessThanOrEqual(p[0], xMax) &&
+ jsnums.lessThanOrEqual(yMin, p[1]) &&
+ jsnums.lessThanOrEqual(p[1], yMax);
}
function dist(a, b) {
return jsnums.add(
- jsnums.sqr(jsnums.subtract(a[0], b[0], RUNTIME.NumberErrbacks)),
- jsnums.sqr(jsnums.subtract(a[1], b[1], RUNTIME.NumberErrbacks)), RUNTIME.NumberErrbacks);
+ jsnums.sqr(jsnums.subtract(a[0], b[0])),
+ jsnums.sqr(jsnums.subtract(a[1], b[1])));
}
function nearest(candidates, origin) {
@@ -636,7 +636,7 @@
var optimal = null;
candidates.forEach(function (candidate) {
var distance = dist(candidate, origin);
- if (optimal === null || jsnums.lessThan(distance, optimal, RUNTIME.NumberErrbacks)) {
+ if (optimal === null || jsnums.lessThan(distance, optimal)) {
optimal = distance;
ans = candidate;
}
@@ -645,7 +645,7 @@
}
function equal(a, b) {
- return jsnums.lessThanOrEqual(a, b, RUNTIME.NumberErrbacks) && jsnums.lessThanOrEqual(b, a, RUNTIME.NumberErrbacks);
+ return jsnums.lessThanOrEqual(a, b) && jsnums.lessThanOrEqual(b, a);
}
function findPointOnEdge(near, far) {
@@ -677,15 +677,15 @@
x = (y - c) / m [4] [rewrite 3]
*/
- var m = jsnums.divide(jsnums.subtract(near[1], far[1], RUNTIME.NumberErrbacks), jsnums.subtract(near[0], far[0], RUNTIME.NumberErrbacks));
- var c = jsnums.subtract(near[1], jsnums.multiply(m, near[0], RUNTIME.NumberErrbacks), RUNTIME.NumberErrbacks);
+ var m = jsnums.divide(jsnums.subtract(near[1], far[1]), jsnums.subtract(near[0], far[0]));
+ var c = jsnums.subtract(near[1], jsnums.multiply(m, near[0]));
var f = function (x) {
- return jsnums.add(jsnums.multiply(m, x, RUNTIME.NumberErrbacks), c, RUNTIME.NumberErrbacks);
+ return jsnums.add(jsnums.multiply(m, x), c);
};
var g = function (y) {
- return jsnums.divide(jsnums.subtract(y, c, RUNTIME.NumberErrbacks), m, RUNTIME.NumberErrbacks);
+ return jsnums.divide(jsnums.subtract(y, c), m);
};
candidates = [
@@ -702,10 +702,10 @@
}
return nearest(candidates.filter(function (p) {
- return jsnums.lessThanOrEqual(pxMin, p[0], RUNTIME.NumberErrbacks) &&
- jsnums.lessThanOrEqual(p[0], pxMax, RUNTIME.NumberErrbacks) &&
- jsnums.lessThanOrEqual(pyMin, p[1], RUNTIME.NumberErrbacks) &&
- jsnums.lessThanOrEqual(p[1], pyMax, RUNTIME.NumberErrbacks);
+ return jsnums.lessThanOrEqual(pxMin, p[0]) &&
+ jsnums.lessThanOrEqual(p[0], pxMax) &&
+ jsnums.lessThanOrEqual(pyMin, p[1]) &&
+ jsnums.lessThanOrEqual(p[1], pyMax);
}), near);
}
@@ -716,8 +716,8 @@
function toJSOptions(options) {
return {
color: CLIB.libColor.convertColor(gf(options, 'color')),
- size: jsnums.toFixnum(gf(options, 'size'), RUNTIME.NumberErrbacks),
- opacity: jsnums.toFixnum(gf(options, 'opacity'), RUNTIME.NumberErrbacks),
+ size: jsnums.toFixnum(gf(options, 'size')),
+ opacity: jsnums.toFixnum(gf(options, 'opacity')),
tip: RUNTIME.isPyretTrue(gf(options, 'tip')),
};
}
@@ -913,7 +913,7 @@
var sum = tab.map(function (row) { return row[1]; })
.reduce(function (a, b) {
- return jsnums.add(a, b, RUNTIME.NumberErrbacks);
+ return jsnums.add(a, b);
});
var valueScaler = libNum.scaler(0, sum, 0, 100, true);
@@ -1084,7 +1084,7 @@
.scale(y)
.orient('left')
.tickFormat(function (d) {
- return prettyNumToStringDigitsForAxis(yAxisDisplayScaler(jsnums.fromFixnum(d, RUNTIME.NumberErrbacks)));
+ return prettyNumToStringDigitsForAxis(yAxisDisplayScaler(jsnums.fromFixnum(d)));
});
x0.domain(data.map(function (d) { return d.label; }));
@@ -1352,7 +1352,7 @@
.scale(y)
.orient('left')
.tickFormat(function (d) {
- return prettyNumToStringDigitsForAxis(yAxisDisplayScaler(jsnums.fromFixnum(d, RUNTIME.NumberErrbacks)));
+ return prettyNumToStringDigitsForAxis(yAxisDisplayScaler(jsnums.fromFixnum(d)));
});
x0.domain(data.map(function (d) { return d.label; }));
diff --git a/src/web/js/trove/world.js b/src/web/js/trove/world.js
index f4a1f0d0..ab55d62f 100644
--- a/src/web/js/trove/world.js
+++ b/src/web/js/trove/world.js
@@ -5,7 +5,7 @@
{ "import-type": "builtin", "name": "valueskeleton" },
{ "import-type": "builtin", "name": "reactors" }
],
- nativeRequires: ["pyret-base/js/js-numbers"],
+ nativeRequires: [],
provides: {
shorthands: {
"WCOofA": ["tyapp", ["local", "WorldConfigOption"], [["tid", "a"]]],
@@ -52,7 +52,8 @@
"WorldConfigOption": ["data", "WorldConfigOption", ["a"], [], {}]
}
},
- theModule: function(runtime, namespace, uri, imageLibraryLib, rawJsworld, VSlib, reactors, jsnums) {
+ theModule: function(runtime, namespace, uri, imageLibraryLib, rawJsworld, VSlib, reactors) {
+ var jsnums = runtime.jsnums;
var imageLibrary = runtime.getField(imageLibraryLib, "internal");
var isImage = imageLibrary.isImage;
var VS = runtime.getField(VSlib, "values");