I'm using a Cloudflare Worker to load data from Backblaze B2. It generates a basic HTML page like this:
<html>
<head><meta name="viewport" content="width=device-width"></head>
<body>
<video>
<source src="myvideo.mp4" type="video/mp4">
</video>
</body>
</html>
I'm trying to modify the script so the browser will load a AV1-encoded version, if supported. If it is not then fallback to the WebM/VP9 version.
The HTML would look like the code below, but I cannot figure out how to modify the script correctly:
<html>
<head><meta name="viewport" content="width=device-width"></head>
<body>
<video>
<source src="myvideo.mp4" type="video/mp4">
<source src="myvideo.webm" type="video/webm">
</video>
</body>
</html>
The Worker code I'm using is:
"use strict";
(() => {
// node_modules/aws4fetch/dist/aws4fetch.esm.mjs
var encoder = new TextEncoder();
var HOST_SERVICES = {
appstream2: "appstream",
cloudhsmv2: "cloudhsm",
email: "ses",
marketplace: "aws-marketplace",
mobile: "AWSMobileHubService",
pinpoint: "mobiletargeting",
queue: "sqs",
"git-codecommit": "codecommit",
"mturk-requester-sandbox": "mturk-requester",
"personalize-runtime": "personalize"
};
var UNSIGNABLE_HEADERS = /* #__PURE__ */ new Set([
"authorization",
"content-type",
"content-length",
"user-agent",
"presigned-expires",
"expect",
"x-amzn-trace-id",
"range",
"connection"
]);
var AwsClient = class {
constructor({ accessKeyId, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }) {
if (accessKeyId == null)
throw new TypeError("accessKeyId is a required option");
if (secretAccessKey == null)
throw new TypeError("secretAccessKey is a required option");
this.accessKeyId = accessKeyId;
this.secretAccessKey = secretAccessKey;
this.sessionToken = sessionToken;
this.service = service;
this.region = region;
this.cache = cache || /* #__PURE__ */ new Map();
this.retries = retries != null ? retries : 10;
this.initRetryMs = initRetryMs || 50;
}
async sign(input, init) {
if (input instanceof Request) {
const { method, url, headers, body } = input;
init = Object.assign({ method, url, headers }, init);
if (init.body == null && headers.has("Content-Type")) {
init.body = body != null && headers.has("X-Amz-Content-Sha256") ? body : await input.clone().arrayBuffer();
}
input = url;
}
const signer = new AwsV4Signer(Object.assign({ url: input }, init, this, init && init.aws));
const signed = Object.assign({}, init, await signer.sign());
delete signed.aws;
try {
return new Request(signed.url.toString(), signed);
} catch (e) {
if (e instanceof TypeError) {
return new Request(signed.url.toString(), Object.assign({ duplex: "half" }, signed));
}
throw e;
}
}
async fetch(input, init) {
for (let i = 0; i <= this.retries; i++) {
const fetched = fetch(await this.sign(input, init));
if (i === this.retries) {
return fetched;
}
const res = await fetched;
if (res.status < 500 && res.status !== 429) {
return res;
}
await new Promise((resolve) => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i)));
}
throw new Error("An unknown error occurred, ensure retries is not negative");
}
};
var AwsV4Signer = class {
constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) {
if (url == null)
throw new TypeError("url is a required option");
if (accessKeyId == null)
throw new TypeError("accessKeyId is a required option");
if (secretAccessKey == null)
throw new TypeError("secretAccessKey is a required option");
this.method = method || (body ? "POST" : "GET");
this.url = new URL(url);
this.headers = new Headers(headers || {});
this.body = body;
this.accessKeyId = accessKeyId;
this.secretAccessKey = secretAccessKey;
this.sessionToken = sessionToken;
let guessedService, guessedRegion;
if (!service || !region) {
[guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers);
}
this.service = service || guessedService || "";
this.region = region || guessedRegion || "us-east-1";
this.cache = cache || /* #__PURE__ */ new Map();
this.datetime = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, "");
this.signQuery = signQuery;
this.appendSessionToken = appendSessionToken || this.service === "iotdevicegateway";
this.headers.delete("Host");
if (this.service === "s3" && !this.signQuery && !this.headers.has("X-Amz-Content-Sha256")) {
this.headers.set("X-Amz-Content-Sha256", "UNSIGNED-PAYLOAD");
}
const params = this.signQuery ? this.url.searchParams : this.headers;
params.set("X-Amz-Date", this.datetime);
if (this.sessionToken && !this.appendSessionToken) {
params.set("X-Amz-Security-Token", this.sessionToken);
}
this.signableHeaders = ["host", ...this.headers.keys()].filter((header) => allHeaders || !UNSIGNABLE_HEADERS.has(header)).sort();
this.signedHeaders = this.signableHeaders.join(";");
this.canonicalHeaders = this.signableHeaders.map((header) => header + ":" + (header === "host" ? this.url.host : (this.headers.get(header) || "").replace(/\s+/g, " "))).join("\n");
this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, "aws4_request"].join("/");
if (this.signQuery) {
if (this.service === "s3" && !params.has("X-Amz-Expires")) {
params.set("X-Amz-Expires", "86400");
}
params.set("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
params.set("X-Amz-Credential", this.accessKeyId + "/" + this.credentialString);
params.set("X-Amz-SignedHeaders", this.signedHeaders);
}
if (this.service === "s3") {
try {
this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, " "));
} catch (e) {
this.encodedPath = this.url.pathname;
}
} else {
this.encodedPath = this.url.pathname.replace(/\/+/g, "/");
}
if (!singleEncode) {
this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, "/");
}
this.encodedPath = encodeRfc3986(this.encodedPath);
const seenKeys = /* #__PURE__ */ new Set();
this.encodedSearch = [...this.url.searchParams].filter(([k]) => {
if (!k)
return false;
if (this.service === "s3") {
if (seenKeys.has(k))
return false;
seenKeys.add(k);
}
return true;
}).map((pair) => pair.map((p) => encodeRfc3986(encodeURIComponent(p)))).sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0).map((pair) => pair.join("=")).join("&");
}
async sign() {
if (this.signQuery) {
this.url.searchParams.set("X-Amz-Signature", await this.signature());
if (this.sessionToken && this.appendSessionToken) {
this.url.searchParams.set("X-Amz-Security-Token", this.sessionToken);
}
} else {
this.headers.set("Authorization", await this.authHeader());
}
return {
method: this.method,
url: this.url,
headers: this.headers,
body: this.body
};
}
async authHeader() {
return [
"AWS4-HMAC-SHA256 Credential=" + this.accessKeyId + "/" + this.credentialString,
"SignedHeaders=" + this.signedHeaders,
"Signature=" + await this.signature()
].join(", ");
}
async signature() {
const date = this.datetime.slice(0, 8);
const cacheKey = [this.secretAccessKey, date, this.region, this.service].join();
let kCredentials = this.cache.get(cacheKey);
if (!kCredentials) {
const kDate = await hmac("AWS4" + this.secretAccessKey, date);
const kRegion = await hmac(kDate, this.region);
const kService = await hmac(kRegion, this.service);
kCredentials = await hmac(kService, "aws4_request");
this.cache.set(cacheKey, kCredentials);
}
return buf2hex(await hmac(kCredentials, await this.stringToSign()));
}
async stringToSign() {
return [
"AWS4-HMAC-SHA256",
this.datetime,
this.credentialString,
buf2hex(await hash(await this.canonicalString()))
].join("\n");
}
async canonicalString() {
return [
this.method.toUpperCase(),
this.encodedPath,
this.encodedSearch,
this.canonicalHeaders + "\n",
this.signedHeaders,
await this.hexBodyHash()
].join("\n");
}
async hexBodyHash() {
let hashHeader = this.headers.get("X-Amz-Content-Sha256") || (this.service === "s3" && this.signQuery ? "UNSIGNED-PAYLOAD" : null);
if (hashHeader == null) {
if (this.body && typeof this.body !== "string" && !("byteLength" in this.body)) {
throw new Error("body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header");
}
hashHeader = buf2hex(await hash(this.body || ""));
}
return hashHeader;
}
};
async function hmac(key, string) {
const cryptoKey = await crypto.subtle.importKey(
"raw",
typeof key === "string" ? encoder.encode(key) : key,
{ name: "HMAC", hash: { name: "SHA-256" } },
false,
["sign"]
);
return crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(string));
}
async function hash(content) {
return crypto.subtle.digest("SHA-256", typeof content === "string" ? encoder.encode(content) : content);
}
function buf2hex(buffer) {
return Array.prototype.map.call(new Uint8Array(buffer), (x) => ("0" + x.toString(16)).slice(-2)).join("");
}
function encodeRfc3986(urlEncodedStr) {
return urlEncodedStr.replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
}
function guessServiceRegion(url, headers) {
const { hostname, pathname } = url;
if (hostname.endsWith(".r2.cloudflarestorage.com")) {
return ["s3", "auto"];
}
if (hostname.endsWith(".backblazeb2.com")) {
const match2 = hostname.match(/^(?:[^.]+\.)?s3\.([^.]+)\.backblazeb2\.com$/);
return match2 != null ? ["s3", match2[1]] : ["", ""];
}
const match = hostname.replace("dualstack.", "").match(/([^.]+)\.(?:([^.]*)\.)?amazonaws\.com(?:\.cn)?$/);
let [service, region] = (match || ["", ""]).slice(1, 3);
if (region === "us-gov") {
region = "us-gov-west-1";
} else if (region === "s3" || region === "s3-accelerate") {
region = "us-east-1";
service = "s3";
} else if (service === "iot") {
if (hostname.startsWith("iot.")) {
service = "execute-api";
} else if (hostname.startsWith("data.jobs.iot.")) {
service = "iot-jobs-data";
} else {
service = pathname === "/mqtt" ? "iotdevicegateway" : "iotdata";
}
} else if (service === "autoscaling") {
const targetPrefix = (headers.get("X-Amz-Target") || "").split(".")[0];
if (targetPrefix === "AnyScaleFrontendService") {
service = "application-autoscaling";
} else if (targetPrefix === "AnyScaleScalingPlannerFrontendService") {
service = "autoscaling-plans";
}
} else if (region == null && service.startsWith("s3-")) {
region = service.slice(3).replace(/^fips-|^external-1/, "");
service = "s3";
} else if (service.endsWith("-fips")) {
service = service.slice(0, -5);
} else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) {
[service, region] = [region, service];
}
return [HOST_SERVICES[service] || service, region];
}
// index.js
var UNSIGNABLE_HEADERS2 = [
"x-forwarded-proto",
"x-real-ip"
];
function filterHeaders(headers) {
return Array.from(headers.entries()).filter((pair) => !UNSIGNABLE_HEADERS2.includes(pair[0]) && !pair[0].startsWith("cf-"));
}
async function handleRequest(event, client2) {
const request = event.request;
if (!["GET", "HEAD"].includes(request.method)) {
return new Response(null, {
status: 405,
statusText: "Method Not Allowed"
});
}
const url = new URL(request.url);
let path = url.pathname.replace(/^\//, "");
path = path.replace(/\/$/, "");
const pathSegments = path.split("/");
if (ALLOW_LIST_BUCKET !== "true") {
if (BUCKET_NAME === "$path" && pathSegments[0].length < 2 || BUCKET_NAME !== "$path" && path.length === 0) {
return new Response(null, {
status: 404,
statusText: "Not Found"
});
}
}
switch (BUCKET_NAME) {
case "$path":
url.hostname = B2_ENDPOINT;
break;
break;
case "$host":
url.hostname = url.hostname.split(".")[0] + "." + B2_ENDPOINT;
break;
default:
url.hostname = BUCKET_NAME + "." + B2_ENDPOINT;
break;
}
const headers = filterHeaders(request.headers);
const signedRequest = await client2.sign(url.toString(), {
method: request.method,
headers,
body: request.body
});
return fetch(signedRequest);
}
var endpointRegex = /^s3\.([a-zA-Z0-9-]+)\.backblazeb2\.com$/;
var [, aws_region] = B2_ENDPOINT.match(endpointRegex);
var client = new AwsClient({
"accessKeyId": B2_APPLICATION_KEY_ID,
"secretAccessKey": B2_APPLICATION_KEY,
"service": "s3",
"region": aws_region
});
addEventListener("fetch", function(event) {
event.respondWith(handleRequest(event, client));
});
})();
/**
* #license MIT <https://opensource.org/licenses/MIT>
* #copyright Michael Hart 2022
*/
//# sourceMappingURL=index.js.map
[1]: https://github.com/backblaze-b2-samples/cloudflare-b2
Just started a 'Writing JS for the Web' course with following exercise and code, but getting 'unexpected end of JSON input' message upon completion of the exercise. Any idea what's going wrong? I'm coding in the following Codepen: https://codepen.io/nicolaspatschkowski/pen/GRJBZjy?editors=1111
// Get form elements
const titleInput = document.getElementById('title');
const contentInput = document.getElementById('content');
const submitButton = document.getElementById('submit-button');
const url = 'https://us-central1-open-classrooms-js-for-the-web.cloudfunctions.net/widgets';
// Get DOM elements for showing response
const responseMessage = document.getElementById('response-message');
const responseTitle = document.getElementById('response-title');
const responseId = document.getElementById('response-id');
const responseContent = document.getElementById('response-content');
submitButton.addEventListener('click', ($event) => {
$event.preventDefault();
const post = {
title: titleInput.value,
content: contentInput.value
};
submitFormData(post);
});
function makeRequest(data) {
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open('POST', url + '/create-post');
request.onreadystatechange = () => {
if (request.readyState === 4) {
if (request.status === 201) {
resolve(JSON.parse(request.response));
} else {
reject(JSON.parse(request.response));
}
}
};
request.setRequestHeader('Content-Type', 'application/json');
request.send(JSON.stringify(data));
});
}
async function submitFormData(post) {
try {
const requestPromise = makeRequest(post);
const response = await requestPromise;
responseMessage.textContent = response.message;
responseTitle.textContent = response.post.title;
responseId.textContent = response.post.id;
responseContent.textContent = response.post.content;
} catch (errorResponse) { responseMessage.textContent = errorResponse.error;
}
};
I want to backup IndexedDB into json file, and restore it on another machine / browser.
the goal is to fully backup and import website session (localStorage, cookies etc...)
I think that dumpIndexedDb() function works correctly,
the problem occur when I try to get this data and restore it back using restoreIndexedDbDump(data). there's no errors.
but only few databases restored.
I'm pretty sure I was missed something with indexedDB api in the restore operation.
thanks in advance.
function dumpDB(dbName) {
return new Promise((resolve, reject) => {
var request = indexedDB.open(dbName)
request.onerror = e => reject(e)
request.onsuccess = (e) => {
var data = {} // { dbVersion: 1, oName: {keyPath: 'key', data: [{},{}]} }
var db = e.target.result
var dbVersion = db.version
data['dbVersion'] = dbVersion
data['objectStores'] = {}
var oStores = db.objectStoreNames
for (let osName of oStores) {
var transaction = db.transaction(osName, "readonly")
var oStore = transaction.objectStore(osName)
var keyPath = oStore.keyPath
data['objectStores'][osName] = {}
data['objectStores'][osName]['keyPath'] = keyPath
var request = oStore.getAll()
request.onerror = e => reject(e)
request.onsuccess = (e) => {
data['objectStores'][osName]['data'] = e.target.result
}
}
db.close()
resolve(data)
}
})
}
async function dumpIndexedDb() {
var data = {}
var dbs = await indexedDB.databases() // not working in firefox, tested in chrome.
for (let db of dbs) {
var dbData = await dumpDB(db.name)
data[db.name] = dbData
}
return data // {dbName: { dbVersion: 1, oName: {keyPath: 'key', data: [{},{}]} } }
}
function restoreObjectStore(dbName, version, oName, keyPath, data) {
return new Promise((resolve, reject) => {
request = window.indexedDB.open(dbName, version);
request.onerror = e => reject(e)
request.onupgradeneeded = function (e) {
try {
var db = e.target.result
if (keyPath) var objectStore = db.createObjectStore(oName, { keyPath: keyPath, autoIncrement: false })
else var objectStore = db.createObjectStore(oName, { autoIncrement: false })
for (let row of data) {
console.log(`adding row of ${oName}`)
}
objectStore.transaction.commit()
db.close()
resolve("ok")
} catch(e) {
reject(e)
}
}
})
}
async function restoreIndexedDbDump(data) {
for (let dbName in data) {
console.log(`deleting db ${dbName}`)
indexedDB.deleteDatabase(dbName)
for (let oStoreName in data[dbName]['objectStores']) {
let odata = data[dbName]['objectStores'][oStoreName]['data']
let dbVersion = data[dbName]['dbVersion']
if ( data[dbName]['objectStores'][oStoreName]['keyPath'] ) {
var keyPath = data[dbName]['objectStores'][oStoreName]['keyPath']
await restoreObjectStore(dbName, dbVersion, oStoreName, keyPath, odata)
}
else {
await restoreObjectStore(dbName, dbVersion, oStoreName, null, odata)
}
}
}
}
async function testBackupandRestore() {
var data = await dumpIndexedDb()
await restoreIndexedDbDump(data)
}
I'm working on a prototype of a MagicMirror Module that pulls data in from this json feed https://jsonplaceholder.typicode.com/todos/1. It passes fine on json parse payload But fails somewhere in within tArray length with cannot ready property 'length' of undefined. I'm suspecting the JSON isn't being carried to tArray properly, but i'm a at a loss. Below are my two files!
node_helper.js
'use strict';
/* Magic Mirror
* Module:
*/
const NodeHelper = require('node_helper');
var request = require('request');
var moment = require('moment');
module.exports = NodeHelper.create({
start: function() {
this.started = false;
this.config = null;
},
getData: function() {
var self = this;
var myUrl = "https://jsonplaceholder.typicode.com/todos/1";
request({
url: myUrl,
method: 'GET',
headers: { 'API_TOKEN': this.config.apiKey }
}, function (error, response, body) {
console.log(body);
if (!error && response.statusCode == 200) {
self.sendSocketNotification("DATA", body);
}
});
setTimeout(function() { self.getData(); }, this.config.refreshInterval);
},
socketNotificationReceived: function(notification, payload) {
var self = this;
if (notification === 'CONFIG' && self.started == false) {
self.config = payload;
self.sendSocketNotification("STARTED", true);
self.getData();
self.started = true;
}
}
});
MMM-Fi.js
/* Magic Mirror
*/
Module.register('MMM-Fi',{
defaults: {
animationSpeed: 1000,
refreshInterval: 1000 * 60, //refresh every minute
updateInterval: 1000 * 3600, //update every hour
lang: config.language,
initialLoadDelay: 0, // 0 seconds delay
retryDelay: 5,
},
// Define required scripts.
getScripts: function() {
return ["moment.js", "font-awesome.css"];
},
getStyles: function() {
return ['MMM-Fi.css'];
},
start: function() {
Log.info('Starting module: ' + this.name);
this.loaded = false;
this.sendSocketNotification('CONFIG', this.config);
},
getDom: function() {
var wrapper = document.createElement("div");
if (this.config.apiKey === "") {
wrapper.innerHTML = "No Fi <i>apiKey</i> set in config file.";
wrapper.className = "dimmed light small";
return wrapper;
}
if (this.config.googleApiKey === "") {
wrapper.innerHTML = "No Google <i>api Key</i> set in config file.";
wrapper.className = "dimmed light small";
return wrapper;
}
if (!this.loaded) {
wrapper.innerHTML = this.translate('LOADING');
wrapper.className = "dimmed light small";
return wrapper;
}
if (!this.tArray.length) {
wrapper.innerHTML = "No data";
wrapper.className = "dimmed light small";
return wrapper;
}
var table = document.createElement("table");
table.id = "fitable";
table.className = "small thin light";
var row = document.createElement("tr");
var lineHeader = document.createElement("th");
lineHeader.innerHTML = "test2";
lineHeader.colSpan = 2;
row.appendChild(lineHeader);
table.appendChild(row);
for (var i in this.tArray) {
var currentDeparture = this.tArray[i];
console.log(currentDeparture.title)
}
wrapper.appendChild(table);
return wrapper;
},
tFi: function(data) {
if (!data.listT) {
return;
}
this.tArray = [];
for (var i in data.listT) {
var t = data.listT[i];
this.tArray.push({
userId: t.userId,
id: t.id,
title: t.title,
completed: t.completed,
});
}
return;
},
socketNotificationReceived: function(notification, payload) {
if (notification === "STARTED") {
this.updateDom();
}
else if (notification === "DATA") {
this.loaded = true;
this.tFi(JSON.parse(payload));
this.updateDom();
}
}
});
so I have a selector that contains multiple options and below that I have a paragraph the content of which would change in accordance to the selected option in the select menu. I used find method to loop through the array and return the element(which is an object) that satisfy find function. however there is a problem with find method that I could not figure out.
'use strict';
{
function fetchJSON(url, cb) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = () => {
if (xhr.status < 400) {
cb(null, xhr.response);
} else {
cb(new Error(`Network error: ${xhr.status} - ${xhr.statusText}`));
}
};
xhr.onerror = () => cb(new Error('Network request failed'));
xhr.send();
}
function createAndAppend(name, parent, options = {}) {
const elem = document.createElement(name);
parent.appendChild(elem);
Object.keys(options).forEach(key => {
const value = options[key];
if (key === 'text') {
elem.textContent = value;
} else {
elem.setAttribute(key, value);
}
});
return elem;
}
function createLI(root, sel, options = []) {
const select = document.createElement(sel);
root.appendChild(select);
select.innerHTML = options.sort().map(repo => `<option value="${repo.id}">${repo.name}</option>`).join('\n');
select.addEventListener("change", function () {
const chosenRepoId = this.value;
const selectedRepo = options.find(repo => repo.id === chosenRepoId);
document.getElementById('repoInfo').innerHTML = selectedRepo.description;
});
}
function main(url) {
fetchJSON(url, (err, data) => {
const root = document.getElementById('root');
if (err) {
createAndAppend('div', root, { text: err.message, class: 'alert-error' });
} else {
// createAndAppend('pre', root, { text: JSON.stringify(data, null, 2) });
createLI(root, 'select', data);
}
});
}
const HYF_REPOS_URL = 'https://api.github.com/orgs/HackYourFuture/repos?per_page=100';
window.onload = () => main(HYF_REPOS_URL);
}
````js
this.value returns string.
In the comparison - const selectedRepo = options.find(repo => repo.id === chosenRepoId);
you are using strict equality operator and your chosenRepoId is numeric. So, first convert the selected value to number (use + or parseInt).
const chosenRepoId = +this.value;
const selectedRepo = options.find(repo => repo.id === chosenRepoId);