Commit 2184511e authored by Domenic Denicola's avatar Domenic Denicola Committed by Commit Bot

[Layered API] Update async local storage implementation and tests

This CL updates the implementation and tests to be adapted versions of
https://github.com/domenic/async-local-storage/tree/73580d0151f04849ca9e691807907e95c4d17d12/prototype.

Change-Id: I9071883a6fef8ef97c405a9a749bf9c2d3cfb58a
Reviewed-on: https://chromium-review.googlesource.com/1171573Reviewed-by: default avatarJoshua Bell <jsbell@chromium.org>
Reviewed-by: default avatarHiroshige Hayashizaki <hiroshige@chromium.org>
Commit-Queue: Domenic Denicola <domenic@chromium.org>
Cr-Commit-Position: refs/heads/master@{#584064}
parent b1328fe9
<!DOCTYPE html>
<meta charset="utf-8">
<title>Async local storage API surface</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script type="module">
import { storage, StorageArea } from "std:async-local-storage";
import * as classAssert from "./helpers/class-assert.js";
import { testWithArea } from "./helpers/als-tests.js";
test(() => {
classAssert.isConstructor(StorageArea);
classAssert.functionName(StorageArea, "StorageArea");
classAssert.functionLength(StorageArea, 1);
classAssert.hasClassPrototype(StorageArea);
classAssert.hasPrototypeConstructorLink(StorageArea);
classAssert.propertyKeys(StorageArea, ["length", "prototype", "name"], []);
}, "StorageArea constructor");
test(() => {
classAssert.propertyKeys(StorageArea.prototype, [
"constructor", "set", "get", "has", "delete", "clear",
"keys", "values", "entries", "backingStore"
], []);
classAssert.methods(StorageArea.prototype, {
set: 2,
get: 1,
has: 1,
delete: 1,
clear: 0,
keys: 0,
values: 0,
entries: 0
});
classAssert.accessors(StorageArea.prototype, {
backingStore: ["get"]
});
}, "StorageArea.prototype methods and properties");
testWithArea(async area => {
classAssert.propertyKeys(area, [], []);
}, "Instances don't have any properties")
test(() => {
assert_equals(storage instanceof StorageArea, true, "instanceof");
assert_equals(storage.constructor, StorageArea, ".constructor property");
assert_equals(Object.getPrototypeOf(storage), StorageArea.prototype, "[[Prototype]]");
}, "Built-in storage export is a StorageArea");
testWithArea(async area => {
assert_false(Symbol.toStringTag in StorageArea.prototype,
"Symbol.toStringTag must not be in the prototype");
assert_equals(Object.prototype.toString.call(StorageArea.prototype), "[object Object]",
"The prototype must not be customized");
assert_equals(Object.prototype.toString.call(area), "[object Object]",
"A constructed StorageArea must not be customized");
assert_equals(Object.prototype.toString.call(storage), "[object Object]",
"The default storage area must not be customized");
}, "No custom toStringTag");
</script>
import { StorageArea, storage as defaultArea } from "std:async-local-storage";
import { assertArrayCustomEquals } from "./equality-asserters.js";
export function testWithArea(testFn, description) {
promise_test(t => {
const area = new StorageArea(description);
t.add_cleanup(t => area.clear());
return testFn(area, t);
}, description);
}
export function testWithDefaultArea(testFn, description) {
promise_test(t => {
t.add_cleanup(t => defaultArea.clear());
return testFn(defaultArea, t);
}, description);
}
// These two functions take a key/value and use them to test
// set()/get()/delete()/has()/keys()/values()/entries(). The keyEqualityAsserter should be a
// function from ./equality-asserters.js.
export function testVariousMethodsWithDefaultArea(label, key, value, keyEqualityAsserter) {
testWithDefaultArea(testVariousMethodsInner(key, value, keyEqualityAsserter), label);
}
export function testVariousMethods(label, key, value, keyEqualityAsserter) {
testWithArea(testVariousMethodsInner(key, value, keyEqualityAsserter), label);
}
function testVariousMethodsInner(key, value, keyEqualityAsserter) {
return async area => {
await assertPromiseEquals(area.set(key, value), undefined, "set()", "undefined");
await assertPromiseEquals(area.get(key), value, "get()", "the set value");
await assertPromiseEquals(area.has(key), true, "has()", "true");
const keysPromise = area.keys();
assertIsPromise(keysPromise, "keys()");
assertArrayCustomEquals(await keysPromise, [key], keyEqualityAsserter, "keys() must have the key");
const valuesPromise = area.values();
assertIsPromise(valuesPromise);
assert_array_equals(await valuesPromise, [value], "values() must have the value");
const entriesPromise = area.entries();
assertIsPromise(entriesPromise, "entries()");
const entries = await entriesPromise;
assert_true(Array.isArray(entries), "entries() must give an array");
assert_equals(entries.length, 1, "entries() must have only one value");
assert_true(Array.isArray(entries[0]), "entries() 0th element must be an array");
assert_equals(entries[0].length, 2, "entries() 0th element must have 2 elements");
keyEqualityAsserter(entries[0][0], key, "entries() 0th element's 0th element must be the key");
assert_equals(entries[0][1], value, "entries() 0th element's 1st element must be the value");
await assertPromiseEquals(area.delete(key), undefined, "delete()", "undefined");
await assertPromiseEquals(area.get(key), undefined, "get()", "undefined after deleting");
await assertPromiseEquals(area.has(key), false, "has()", "false after deleting");
};
}
async function assertPromiseEquals(promise, expected, label, expectedLabel) {
assertIsPromise(promise, label);
assert_equals(await promise, expected, label + " must fulfill with " + expectedLabel);
}
function assertIsPromise(promise, label) {
assert_equals(promise.constructor, Promise, label + " must return a promise");
}
export function isConstructor(o) {
assert_equals(typeof o, "function", "Must be a function according to typeof");
assert_true(isConstructorTest(o), "Must be a constructor according to the meta-object protocol");
assert_throws(new TypeError(), () => o(), "Attempting to call (not construct) must throw");
}
export function functionLength(o, expected, label) {
const lengthExpected = { writable: false, enumerable: false, configurable: true };
const { value } = propertyDescriptor(o, "length", lengthExpected);
assert_equals(value, expected, `${formatLabel(label)}length value`);
}
export function functionName(o, expected, label) {
const lengthExpected = { writable: false, enumerable: false, configurable: true };
const { value } = propertyDescriptor(o, "name", lengthExpected);
assert_equals(value, expected, `${formatLabel(label)}name value`);
}
export function hasClassPrototype(o) {
const prototypeExpected = { writable: false, enumerable: false, configurable: false };
const { value } = propertyDescriptor(o, "prototype", prototypeExpected);
assert_equals(typeof value, "object", "prototype must be an object");
assert_not_equals(value, null, "prototype must not be null");
}
export function hasPrototypeConstructorLink(klass) {
const constructorExpected = { writable: true, enumerable: false, configurable: true };
const { value } = propertyDescriptor(klass.prototype, "constructor", constructorExpected);
assert_equals(value, klass, "constructor property must match");
}
export function propertyKeys(o, expectedNames, expectedSymbols, label) {
label = formatLabel(label);
assert_array_equals(Object.getOwnPropertyNames(o), expectedNames, `${label}property names`);
assert_array_equals(Object.getOwnPropertySymbols(o), expectedSymbols,
`${label}property symbols`);
}
export function methods(o, expectedMethods) {
for (const [name, length] of Object.entries(expectedMethods)) {
method(o, name, length);
}
}
export function accessors(o, expectedAccessors) {
for (const [name, accessorTypes] of Object.entries(expectedAccessors)) {
accessor(o, name, accessorTypes);
}
}
function method(o, prop, length) {
const methodExpected = { writable: true, enumerable: false, configurable: true };
const { value } = propertyDescriptor(o, prop, methodExpected);
assert_equals(typeof value, "function", `${prop} method must be a function according to typeof`);
assert_false(isConstructorTest(value),
`${prop} method must not be a constructor according to the meta-object protocol`);
functionLength(value, length, prop);
functionName(value, prop, prop);
propertyKeys(value, ["length", "name"], [], prop);
}
function accessor(o, prop, expectedAccessorTypes) {
const accessorExpected = { enumerable: false, configurable: true };
const propDesc = propertyDescriptor(o, prop, accessorExpected);
for (const possibleType of ["get", "set"]) {
const accessorFunc = propDesc[possibleType];
if (expectedAccessorTypes.includes(possibleType)) {
const label = `${prop}'s ${possibleType}ter`;
assert_equals(typeof accessorFunc, "function",
`${label} must be a function according to typeof`);
assert_false(isConstructorTest(accessorFunc),
`${label} must not be a constructor according to the meta-object protocol`);
functionLength(accessorFunc, possibleType === "get" ? 0 : 1, label);
functionName(accessorFunc, `${possibleType} ${prop}`, label);
propertyKeys(accessorFunc, ["length", "name"], [], label);
} else {
assert_equals(accessorFunc, undefined, `${prop} must not have a ${possibleType}ter`);
}
}
}
function propertyDescriptor(obj, prop, mustMatch) {
const propDesc = Object.getOwnPropertyDescriptor(obj, prop);
for (const key in Object.keys(mustMatch)) {
assert_equals(propDesc[key], mustMatch[key], `${prop} ${key}`);
}
return propDesc;
}
function isConstructorTest(o) {
try {
new (new Proxy(o, {construct: () => ({})}));
return true;
} catch (e) {
return false;
}
}
function formatLabel(label) {
return label !== undefined ? ` ${label}` : "";
}
export function assertEqualDates(actual, expected, label) {
assert_equals(expected.constructor, Date,
"assertEqualDates usage check: expected must be a Date");
const labelPart = label === undefined ? "" : `${label}: `;
assert_equals(actual.constructor, Date, `${labelPart}must be a Date`);
assert_equals(actual.valueOf(), expected.valueOf(), `${labelPart}timestamps must match`);
}
export function assertEqualArrayBuffers(actual, expected, label) {
assert_equals(expected.constructor, ArrayBuffer,
"assertEqualArrayBuffers usage check: expected must be an ArrayBuffer");
const labelPart = label === undefined ? "" : `${label}: `;
assert_equals(actual.constructor, ArrayBuffer, `${labelPart}must be an ArrayBuffer`);
assert_array_equals(new Uint8Array(actual), new Uint8Array(expected), `${labelPart}must match`);
}
export function assertArrayBufferEqualsABView(actual, expected, label) {
assert_true(ArrayBuffer.isView(expected),
"assertArrayBufferEqualsABView usage check: expected must be an ArrayBuffer view");
assertEqualArrayBuffers(actual, expected.buffer, label);
}
export function assertArrayCustomEquals(actual, expected, equalityAsserter, label) {
assert_true(Array.isArray(expected),
"assertArrayCustomEquals usage check: expected must be an Array");
const labelPart = label === undefined ? "" : `${label}: `;
assert_true(Array.isArray(actual), `${labelPart}must be an array`);
assert_equals(actual.length, expected.length, `${labelPart}length must be as expected`);
for (let i = 0; i < actual.length; ++i) {
equalityAsserter(actual[i], expected[i], `${labelPart}index ${i}`);
}
}
<!DOCTYPE html>
<meta charset="utf-8">
<title>Async local storage: tests against various key types</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script type="module">
import { testWithArea, testVariousMethods } from "./helpers/als-tests.js";
import { assertEqualDates, assertEqualArrayBuffers, assertArrayBufferEqualsABView }
from "./helpers/equality-asserters.js";
const invalidKeys = {
"undefined": undefined,
"null": null,
"a boolean": true,
"a symbol": Symbol("a symbol"),
"an object": { an: "object" },
"a function": () => {},
"a regexp": /foo/,
"a Map": new Map(),
"a Set": new Set(),
"an IDBKeyRange": IDBKeyRange.only(5)
};
const validKeys = {
"a number": [5, assert_equals],
"a string": ["a string", assert_equals],
"a Date": [new Date(), assertEqualDates],
"a typed array": [new Uint8Array([1, 2]), assertArrayBufferEqualsABView],
"a DataView": [new DataView(new Uint8Array([3, 4]).buffer), assertArrayBufferEqualsABView],
"an ArrayBuffer": [new Uint8Array([5, 6]).buffer, assertEqualArrayBuffers]
};
const methods = ["set", "get", "has", "delete"];
for (const method of methods) {
testWithArea(async (area, t) => {
for (const [description, key] of Object.entries(invalidKeys)) {
await promise_rejects(t, "DataError", area[method](key), description);
}
}, `${method}: invalid keys`);
testWithArea(async (area, t) => {
for (const [description, key] of Object.entries(invalidKeys)) {
await promise_rejects(t, "DataError", area[method]([key]), description);
}
}, `${method}: invalid keys, nested in arrays`);
testWithArea(async (area, t) => {
for (const [key] of Object.values(validKeys)) {
await area[method](key);
}
}, `${method}: valid keys`);
testWithArea(async (area, t) => {
for (const [key] of Object.values(validKeys)) {
await area[method]([key]);
}
}, `${method}: valid keys, nested in arrays`);
}
for (const [description, [key, equalityAsserter]] of Object.entries(validKeys)) {
testVariousMethods(`Storage methods smoke test: ${description} key`, key, 5, equalityAsserter);
}
</script>
<!DOCTYPE html>
<meta charset="utf-8">
<title>Async local storage: should not work in non-secure contexts when included via import()</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
"use strict";
test(() => {
assert_false(self.isSecureContext, "This test must run in a non-secure context");
}, "Prerequisite check");
promise_test(t => {
return promise_rejects(t, "SecurityError", import("std:async-local-storage"));
});
</script>
<!DOCTYPE html>
<meta charset="utf-8">
<title>Async local storage: should not work in non-secure contexts when included via an import statement</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
"use strict";
setup({ allow_uncaught_exception: true });
test(() => {
assert_false(self.isSecureContext, "This test must run in a non-secure context");
}, "Prerequisite check");
async_test(t => {
window.addEventListener("error", t.step_func_done(errorEvent => {
assert_equals(errorEvent.error.constructor, DOMException, "Must trigger a DOMException");
assert_equals(errorEvent.error.name, "SecurityError",
"The DOMException must be a \"SecurityError\"");
}, { once: true }));
});
</script>
<script type="module">
import "std:async-local-storage";
</script>
<!DOCTYPE html>
<meta charset="utf-8">
<title>Async local storage: should not work in non-secure contexts when included via a script element</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
"use strict";
setup({ allow_uncaught_exception: true });
test(() => {
assert_false(self.isSecureContext, "This test must run in a non-secure context");
}, "Prerequisite check");
async_test(t => {
self.addEventListener("error", t.step_func_done(errorEvent => {
assert_equals(errorEvent.error.constructor, DOMException, "Must trigger a DOMException");
assert_equals(errorEvent.error.name, "SecurityError",
"The DOMException must be a \"SecurityError\"");
}, { once: true }));
});
</script>
<script type="module" src="std:async-local-storage"></script>
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
<script src="/resources/testharnessreport.js"></script> <script src="/resources/testharnessreport.js"></script>
<script type="module"> <script type="module">
import { testVariousMethodsWithDefaultArea } from "./helpers/als-tests.js";
import { storage } from "std:async-local-storage"; import { storage } from "std:async-local-storage";
test(() => { test(() => {
...@@ -21,26 +22,7 @@ test(() => { ...@@ -21,26 +22,7 @@ test(() => {
assert_equals(backingStore.version, 1); assert_equals(backingStore.version, 1);
}, "backingStore returns the correct object"); }, "backingStore returns the correct object");
promise_test(async (t) => { testVariousMethodsWithDefaultArea(
t.add_cleanup(async () => { "Storage methods smoke test with string key and value", "key", "value", assert_equals
await storage.clear(); );
});
assert_equals(await storage.set("key", "value"), undefined);
assert_equals(await storage.get("key"), "value");
assert_equals(await storage.has("key"), true);
assert_array_equals(await storage.keys(), ["key"]);
assert_array_equals(await storage.values(), ["value"]);
const entries = await storage.entries();
assert_true(Array.isArray(entries));
assert_equals(entries.length, 1);
assert_array_equals(entries[0], ["key", "value"]);
assert_equals(await storage.delete("key"), undefined);
assert_equals(await storage.get("key"), undefined);
assert_equals(await storage.has("key"), false);
}, "storage methods work, at least for one entry with string key and value");
</script> </script>
// TODOs/spec-noncompliances: // TODOs/spec-noncompliances:
// - Susceptible to tampering of built-in prototypes and globals. We want to work on tooling to ameliorate that. // - Susceptible to tampering of built-in prototypes and globals. We want to
// - Uses symbols for information hiding but those are interceptable and forgeable. Need private fields/methods. // work on tooling to ameliorate that.
const databaseName = Symbol("[[DatabaseName]]"); // TODO: Use private fields when those ship.
const databasePromise = Symbol("[[DatabasePromise]]"); const databaseName = new WeakMap();
const databasePromise = new WeakMap();
if (!self.isSecureContext) {
throw new DOMException(
'Async local storage is only available in secure contexts',
'SecurityError');
}
export class StorageArea { export class StorageArea {
constructor(name) { constructor(name) {
this[databasePromise] = null; databasePromise.set(this, null);
this[databaseName] = "async-local-storage:" + `${name}`; databaseName.set(this, `async-local-storage:${name}`);
} }
async set(key, value) { async set(key, value) {
throwForDisallowedKey(key); throwForDisallowedKey(key);
return performDatabaseOperation(this, "readwrite", (transaction, store) => { return performDatabaseOperation(this, 'readwrite', (transaction, store) => {
store.put(value, key); store.put(value, key);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
...@@ -28,7 +35,7 @@ export class StorageArea { ...@@ -28,7 +35,7 @@ export class StorageArea {
async get(key) { async get(key) {
throwForDisallowedKey(key); throwForDisallowedKey(key);
return performDatabaseOperation(this, "readonly", (transaction, store) => { return performDatabaseOperation(this, 'readonly', (transaction, store) => {
const request = store.get(key); const request = store.get(key);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
...@@ -41,7 +48,7 @@ export class StorageArea { ...@@ -41,7 +48,7 @@ export class StorageArea {
async has(key) { async has(key) {
throwForDisallowedKey(key); throwForDisallowedKey(key);
return performDatabaseOperation(this, "readonly", (transaction, store) => { return performDatabaseOperation(this, 'readonly', (transaction, store) => {
const request = store.count(key); const request = store.count(key);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
...@@ -54,7 +61,7 @@ export class StorageArea { ...@@ -54,7 +61,7 @@ export class StorageArea {
async delete(key) { async delete(key) {
throwForDisallowedKey(key); throwForDisallowedKey(key);
return performDatabaseOperation(this, "readwrite", (transaction, store) => { return performDatabaseOperation(this, 'readwrite', (transaction, store) => {
store.delete(key); store.delete(key);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
...@@ -66,28 +73,27 @@ export class StorageArea { ...@@ -66,28 +73,27 @@ export class StorageArea {
} }
async clear() { async clear() {
if (!(databasePromise in this)) { if (!databasePromise.has(this)) {
return Promise.reject(new TypeError("Invalid this value")); return Promise.reject(new TypeError('Invalid this value'));
} }
if (this[databasePromise] !== null) { if (databasePromise.get(this) !== null) {
return this[databasePromise].then( return databasePromise.get(this).then(
() => { () => {
this[databasePromise] = null; databasePromise.set(this, null);
return deleteDatabase(this[databaseName]); return deleteDatabase(databaseName.get(this));
}, },
() => { () => {
this[databasePromise] = null; databasePromise.set(this, null);
return deleteDatabase(this[databaseName]); return deleteDatabase(databaseName.get(this));
} });
);
} }
return deleteDatabase(this[databaseName]); return deleteDatabase(databaseName.get(this));
} }
async keys() { async keys() {
return performDatabaseOperation(this, "readonly", (transaction, store) => { return performDatabaseOperation(this, 'readonly', (transaction, store) => {
const request = store.getAllKeys(undefined); const request = store.getAllKeys(undefined);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
...@@ -98,7 +104,7 @@ export class StorageArea { ...@@ -98,7 +104,7 @@ export class StorageArea {
} }
async values() { async values() {
return performDatabaseOperation(this, "readonly", (transaction, store) => { return performDatabaseOperation(this, 'readonly', (transaction, store) => {
const request = store.getAll(undefined); const request = store.getAll(undefined);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
...@@ -109,7 +115,7 @@ export class StorageArea { ...@@ -109,7 +115,7 @@ export class StorageArea {
} }
async entries() { async entries() {
return performDatabaseOperation(this, "readonly", (transaction, store) => { return performDatabaseOperation(this, 'readonly', (transaction, store) => {
const keysRequest = store.getAllKeys(undefined); const keysRequest = store.getAllKeys(undefined);
const valuesRequest = store.getAll(undefined); const valuesRequest = store.getAll(undefined);
...@@ -125,48 +131,44 @@ export class StorageArea { ...@@ -125,48 +131,44 @@ export class StorageArea {
} }
get backingStore() { get backingStore() {
if (!(databasePromise in this)) { if (!databasePromise.has(this)) {
throw new TypeError("Invalid this value"); throw new TypeError('Invalid this value');
} }
return { return {database: databaseName.get(this), store: 'store', version: 1};
database: this[databaseName],
store: "store",
version: 1
};
} }
} }
export const storage = new StorageArea("default"); export const storage = new StorageArea('default');
function performDatabaseOperation(area, mode, steps) { function performDatabaseOperation(area, mode, steps) {
if (!(databasePromise in area)) { if (!databasePromise.has(area)) {
return Promise.reject(new TypeError("Invalid this value")); return Promise.reject(new TypeError('Invalid this value'));
} }
if (area[databasePromise] === null) { if (databasePromise.get(area) === null) {
initializeDatabasePromise(area); initializeDatabasePromise(area);
} }
return area[databasePromise].then(database => { return databasePromise.get(area).then(database => {
const transaction = database.transaction("store", mode); const transaction = database.transaction('store', mode);
const store = transaction.objectStore("store"); const store = transaction.objectStore('store');
return steps(transaction, store); return steps(transaction, store);
}); });
} }
function initializeDatabasePromise(area) { function initializeDatabasePromise(area) {
area[databasePromise] = new Promise((resolve, reject) => { databasePromise.set(area, new Promise((resolve, reject) => {
const request = self.indexedDB.open(area[databaseName], 1); const request = self.indexedDB.open(databaseName.get(area), 1);
request.onsuccess = () => { request.onsuccess = () => {
const database = request.result; const database = request.result;
database.onclose = () => area[databasePromise] = null; database.onclose = () => databasePromise.set(area, null);
database.onversionchange = () => { database.onversionchange = () => {
database.close(); database.close();
area[databasePromise] = null; databasePromise.set(area, null);
} };
resolve(database); resolve(database);
}; };
...@@ -174,16 +176,16 @@ function initializeDatabasePromise(area) { ...@@ -174,16 +176,16 @@ function initializeDatabasePromise(area) {
request.onupgradeneeded = () => { request.onupgradeneeded = () => {
try { try {
request.result.createObjectStore("store"); request.result.createObjectStore('store');
} catch (e) { } catch (e) {
reject(e); reject(e);
} }
}; };
}); }));
} }
function isAllowedAsAKey(value) { function isAllowedAsAKey(value) {
if (typeof value === "number" || typeof value === "string") { if (typeof value === 'number' || typeof value === 'string') {
return true; return true;
} }
...@@ -210,24 +212,26 @@ function isDate(value) { ...@@ -210,24 +212,26 @@ function isDate(value) {
try { try {
Date.prototype.getTime.call(value); Date.prototype.getTime.call(value);
return true; return true;
} catch (e) { // TODO: remove useless binding } catch {
return false; return false;
} }
} }
const byteLengthGetter = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, "byteLength").get; const byteLengthGetter =
Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'byteLength').get;
function isArrayBuffer(value) { function isArrayBuffer(value) {
try { try {
byteLengthGetter.call(value); byteLengthGetter.call(value);
return true; return true;
} catch (e) { // TODO: remove useless binding } catch {
return false; return false;
} }
} }
function throwForDisallowedKey(key) { function throwForDisallowedKey(key) {
if (!isAllowedAsAKey(key)) { if (!isAllowedAsAKey(key)) {
throw new DOMException("The given value is not allowed as a key", "DataError"); throw new DOMException(
'The given value is not allowed as a key', 'DataError');
} }
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment