summaryrefslogtreecommitdiff
path: root/runtime/js/40_permissions.js
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2021-02-25 14:33:09 +1100
committerGitHub <noreply@github.com>2021-02-25 14:33:09 +1100
commit097e9c44f4d4c7daae7d8113c391bd24d29e7119 (patch)
treedd6b8bd34c9e762f42eadc59ebcfcbca7e4faf43 /runtime/js/40_permissions.js
parent90e4c5dcde99c5f0a1aaa18c0ad786613174085b (diff)
feat(runtime): stabilise permissions and add event target capabilities (#9573)
Diffstat (limited to 'runtime/js/40_permissions.js')
-rw-r--r--runtime/js/40_permissions.js151
1 files changed, 136 insertions, 15 deletions
diff --git a/runtime/js/40_permissions.js b/runtime/js/40_permissions.js
index 2b4acb595..d7ed5a433 100644
--- a/runtime/js/40_permissions.js
+++ b/runtime/js/40_permissions.js
@@ -2,57 +2,178 @@
"use strict";
((window) => {
- const core = window.Deno.core;
- const { illegalConstructorKey } = window.__bootstrap.webUtil;
+ const {
+ Event,
+ EventTarget,
+ Deno: { core },
+ __bootstrap: { webUtil: { illegalConstructorKey } },
+ } = window;
+ /**
+ * @typedef StatusCacheValue
+ * @property {PermissionState} state
+ * @property {PermissionStatus} status
+ */
+
+ /** @type {ReadonlyArray<"read" | "write" | "net" | "env" | "run" | "plugin" | "hrtime">} */
+ const permissionNames = [
+ "read",
+ "write",
+ "net",
+ "env",
+ "run",
+ "plugin",
+ "hrtime",
+ ];
+
+ /**
+ * @param {Deno.PermissionDescriptor} desc
+ * @returns {Deno.PermissionState}
+ */
function opQuery(desc) {
return core.jsonOpSync("op_query_permission", desc).state;
}
+ /**
+ * @param {Deno.PermissionDescriptor} desc
+ * @returns {Deno.PermissionState}
+ */
function opRevoke(desc) {
return core.jsonOpSync("op_revoke_permission", desc).state;
}
+ /**
+ * @param {Deno.PermissionDescriptor} desc
+ * @returns {Deno.PermissionState}
+ */
function opRequest(desc) {
return core.jsonOpSync("op_request_permission", desc).state;
}
- class PermissionStatus {
+ class PermissionStatus extends EventTarget {
+ /** @type {{ state: Deno.PermissionState }} */
+ #state;
+
+ /** @type {((this: PermissionStatus, event: Event) => any) | null} */
+ onchange = null;
+
+ /** @returns {Deno.PermissionState} */
+ get state() {
+ return this.#state.state;
+ }
+
+ /**
+ * @param {{ state: Deno.PermissionState }} state
+ * @param {unknown} key
+ */
constructor(state = null, key = null) {
if (key != illegalConstructorKey) {
throw new TypeError("Illegal constructor.");
}
- this.state = state;
+ super();
+ this.#state = state;
+ }
+
+ /**
+ * @param {Event} event
+ * @returns {boolean}
+ */
+ dispatchEvent(event) {
+ let dispatched = super.dispatchEvent(event);
+ if (dispatched && this.onchange) {
+ this.onchange.call(this, event);
+ dispatched = !event.defaultPrevented;
+ }
+ return dispatched;
+ }
+
+ [Symbol.for("Deno.customInspect")](inspect) {
+ return `${this.constructor.name} ${
+ inspect({ state: this.state, onchange: this.onchange })
+ }`;
+ }
+ }
+
+ /** @type {Map<string, StatusCacheValue>} */
+ const statusCache = new Map();
+
+ /**
+ *
+ * @param {Deno.PermissionDescriptor} desc
+ * @param {Deno.PermissionState} state
+ * @returns {PermissionStatus}
+ */
+ function cache(desc, state) {
+ let { name: key } = desc;
+ if ((desc.name === "read" || desc.name === "write") && "path" in desc) {
+ key += `-${desc.path}`;
+ } else if (desc.name === "net" && desc.host) {
+ key += `-${desc.host}`;
+ }
+ if (statusCache.has(key)) {
+ const status = statusCache.get(key);
+ if (status.state !== state) {
+ status.state = state;
+ status.status.dispatchEvent(new Event("change", { cancelable: false }));
+ }
+ return status.status;
}
- // TODO(kt3k): implement onchange handler
+ /** @type {{ state: Deno.PermissionState; status?: PermissionStatus }} */
+ const status = { state };
+ status.status = new PermissionStatus(status, illegalConstructorKey);
+ statusCache.set(key, status);
+ return status.status;
+ }
+
+ /**
+ * @param {unknown} desc
+ * @returns {desc is Deno.PermissionDescriptor}
+ */
+ function isValidDescriptor(desc) {
+ return desc && desc !== null && permissionNames.includes(desc.name);
}
class Permissions {
- constructor(key) {
+ constructor(key = null) {
if (key != illegalConstructorKey) {
throw new TypeError("Illegal constructor.");
}
}
query(desc) {
+ if (!isValidDescriptor(desc)) {
+ return Promise.reject(
+ new TypeError(
+ `The provided value "${desc.name}" is not a valid permission name.`,
+ ),
+ );
+ }
const state = opQuery(desc);
- return Promise.resolve(
- new PermissionStatus(state, illegalConstructorKey),
- );
+ return Promise.resolve(cache(desc, state));
}
revoke(desc) {
+ if (!isValidDescriptor(desc)) {
+ return Promise.reject(
+ new TypeError(
+ `The provided value "${desc.name}" is not a valid permission name.`,
+ ),
+ );
+ }
const state = opRevoke(desc);
- return Promise.resolve(
- new PermissionStatus(state, illegalConstructorKey),
- );
+ return Promise.resolve(cache(desc, state));
}
request(desc) {
+ if (!isValidDescriptor(desc)) {
+ return Promise.reject(
+ new TypeError(
+ `The provided value "${desc.name}" is not a valid permission name.`,
+ ),
+ );
+ }
const state = opRequest(desc);
- return Promise.resolve(
- new PermissionStatus(state, illegalConstructorKey),
- );
+ return Promise.resolve(cache(desc, state));
}
}