diff --git a/cli/js/globals.ts b/cli/js/globals.ts index 1c34e72979..ec314bf68c 100644 --- a/cli/js/globals.ts +++ b/cli/js/globals.ts @@ -2,6 +2,8 @@ import "./lib.deno.shared_globals.d.ts"; +import * as abortController from "./web/abort_controller.ts"; +import * as abortSignal from "./web/abort_signal.ts"; import * as blob from "./web/blob.ts"; import * as consoleTypes from "./web/console.ts"; import * as promiseTypes from "./web/promise.ts"; @@ -207,6 +209,8 @@ export const windowOrWorkerGlobalScopeMethods = { // Other properties shared between WindowScope and WorkerGlobalScope export const windowOrWorkerGlobalScopeProperties = { console: writable(new consoleTypes.Console(core.print)), + AbortController: nonEnumerable(abortController.AbortControllerImpl), + AbortSignal: nonEnumerable(abortSignal.AbortSignalImpl), Blob: nonEnumerable(blob.DenoBlob), File: nonEnumerable(domFile.DomFileImpl), CustomEvent: nonEnumerable(customEvent.CustomEventImpl), diff --git a/cli/js/lib.deno.shared_globals.d.ts b/cli/js/lib.deno.shared_globals.d.ts index f84a056dbc..fd9f3691d6 100644 --- a/cli/js/lib.deno.shared_globals.d.ts +++ b/cli/js/lib.deno.shared_globals.d.ts @@ -1256,6 +1256,16 @@ declare class CustomEvent extends Event { readonly detail: T; } +/** A controller object that allows you to abort one or more DOM requests as and + * when desired. */ +declare class AbortController { + /** Returns the AbortSignal object associated with this object. */ + readonly signal: AbortSignal; + /** Invoking this method will set this object's AbortSignal's aborted flag and + * signal to any observers that the associated activity is to be aborted. */ + abort(): void; +} + interface AbortSignalEventMap { abort: Event; } @@ -1263,10 +1273,8 @@ interface AbortSignalEventMap { /** A signal object that allows you to communicate with a DOM request (such as a * Fetch) and abort it if required via an AbortController object. */ interface AbortSignal extends EventTarget { - /** - * Returns true if this AbortSignal's AbortController has signaled to abort, - * and false otherwise. - */ + /** Returns true if this AbortSignal's AbortController has signaled to abort, + * and false otherwise. */ readonly aborted: boolean; onabort: ((this: AbortSignal, ev: Event) => any) | null; addEventListener( diff --git a/cli/js/tests/abort_controller_test.ts b/cli/js/tests/abort_controller_test.ts new file mode 100644 index 0000000000..ecc1abb881 --- /dev/null +++ b/cli/js/tests/abort_controller_test.ts @@ -0,0 +1,56 @@ +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest(function basicAbortController() { + const controller = new AbortController(); + assert(controller); + const { signal } = controller; + assert(signal); + assertEquals(signal.aborted, false); + controller.abort(); + assertEquals(signal.aborted, true); +}); + +unitTest(function signalCallsOnabort() { + const controller = new AbortController(); + const { signal } = controller; + let called = false; + signal.onabort = (evt): void => { + assert(evt); + assertEquals(evt.type, "abort"); + called = true; + }; + controller.abort(); + assert(called); +}); + +unitTest(function signalEventListener() { + const controller = new AbortController(); + const { signal } = controller; + let called = false; + signal.addEventListener("abort", function (ev) { + assert(this === signal); + assertEquals(ev.type, "abort"); + called = true; + }); + controller.abort(); + assert(called); +}); + +unitTest(function onlyAbortsOnce() { + const controller = new AbortController(); + const { signal } = controller; + let called = 0; + signal.addEventListener("abort", () => called++); + signal.onabort = (): void => { + called++; + }; + controller.abort(); + assertEquals(called, 2); + controller.abort(); + assertEquals(called, 2); +}); + +unitTest(function controllerHasProperToString() { + const actual = Object.prototype.toString.call(new AbortController()); + assertEquals(actual, "[object AbortController]"); +}); diff --git a/cli/js/tests/unit_tests.ts b/cli/js/tests/unit_tests.ts index ba3d6746a8..7d7ec4821d 100644 --- a/cli/js/tests/unit_tests.ts +++ b/cli/js/tests/unit_tests.ts @@ -4,6 +4,7 @@ // // Test runner automatically spawns subprocesses for each required permissions combination. +import "./abort_controller_test.ts"; import "./blob_test.ts"; import "./body_test.ts"; import "./buffer_test.ts"; diff --git a/cli/js/web/abort_controller.ts b/cli/js/web/abort_controller.ts new file mode 100644 index 0000000000..5b0a3af3c8 --- /dev/null +++ b/cli/js/web/abort_controller.ts @@ -0,0 +1,23 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { AbortSignalImpl, signalAbort } from "./abort_signal.ts"; + +export class AbortControllerImpl implements AbortController { + #signal = new AbortSignalImpl(); + + get signal(): AbortSignal { + return this.#signal; + } + + abort(): void { + this.#signal[signalAbort](); + } + + get [Symbol.toStringTag](): string { + return "AbortController"; + } +} + +Object.defineProperty(AbortControllerImpl, "name", { + value: "AbortController", + configurable: true, +}); diff --git a/cli/js/web/abort_signal.ts b/cli/js/web/abort_signal.ts new file mode 100644 index 0000000000..b741d6534d --- /dev/null +++ b/cli/js/web/abort_signal.ts @@ -0,0 +1,58 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { EventImpl } from "./event.ts"; +import { EventTargetImpl } from "./event_target.ts"; + +export const add = Symbol("add"); +export const signalAbort = Symbol("signalAbort"); +export const remove = Symbol("remove"); + +export class AbortSignalImpl extends EventTargetImpl implements AbortSignal { + #aborted?: boolean; + #abortAlgorithms = new Set<() => void>(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onabort: ((this: AbortSignal, ev: Event) => any) | null = null; + + [add](algorithm: () => void): void { + this.#abortAlgorithms.add(algorithm); + } + + [signalAbort](): void { + if (this.#aborted) { + return; + } + this.#aborted = true; + for (const algorithm of this.#abortAlgorithms) { + algorithm(); + } + this.#abortAlgorithms.clear(); + this.dispatchEvent(new EventImpl("abort")); + } + + [remove](algorithm: () => void): void { + this.#abortAlgorithms.delete(algorithm); + } + + constructor() { + super(); + this.addEventListener("abort", (evt: Event) => { + const { onabort } = this; + if (typeof onabort === "function") { + onabort.call(this, evt); + } + }); + } + + get aborted(): boolean { + return Boolean(this.#aborted); + } + + get [Symbol.toStringTag](): string { + return "AbortSignal"; + } +} + +Object.defineProperty(AbortSignalImpl, "name", { + value: "AbortSignal", + configurable: true, +});