Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ anymap = "0.12.1"
base64-simd = "0.8.0"
clap = { version = "4.5.40", features = ["derive"] }
clap_complete = "4.5.54"
console = "0.15.8"
console = "0.15.11"
dprint-core = "0.67.4"
dprint-plugin-typescript = "0.95.5"
dprint-plugin-json = "0.20.0"
indexmap = "2.0"
image = "0.25.5"
indexmap = "2.9.0"
image = "0.25.6"
libsui = "0.10.0"
nova_vm = { git = "https://github.com/trynova/nova", rev = "2f09304d915a3157e73ffc4a7f48bbd34aad3844", features = [
nova_vm = { git = "https://github.com/trynova/nova", rev = "7d1da0bd906ab7b8409878b41bd611f172789ce9", features = [
"typescript"
] }
nu-ansi-term = "0.50.0"
nu-ansi-term = "0.50.1"
owo-colors = "4.2.1"
oxc_ast = "0.72.0"
oxc_allocator = "0.72.0"
Expand All @@ -40,10 +40,10 @@ rand = "0.9.1"
reedline = "0.40.0"
regex = "1.11.1"
ring = "0.17.8"
serde = { version = "1.0.130", features = ["derive"] }
serde = { version = "1.0.219", features = ["derive"] }
thiserror = "2.0.12"
tokio = { version = "1.45.1", features = ["rt", "sync", "time"] }
url = { version = "2", features = ["serde", "expose_internals"] }
url = { version = "2.5.4", features = ["serde", "expose_internals"] }
wgpu = { version = "25.0.2", features = ["wgsl", "webgpu"] }

[profile.release]
Expand Down
221 changes: 184 additions & 37 deletions runtime/src/ext/web/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -859,15 +859,25 @@ class EventTarget {
return;
}
}

if (processedOptions?.signal) {
const signal = processedOptions?.signal;
if (signal.aborted) {
return;
} else {
signal.addEventListener("abort", () => {
this.removeEventListener(type, callback, options);
});
const removeListener = () => {
// Remove the specific listener entry that was added
const listenerList = this[eventTargetData].listeners[type];
if (listenerList) {
for (let i = 0; i < listenerList.length; i++) {
const listener = listenerList[i];
if (listener.callback === callback && listener.options === processedOptions) {
listenerList.splice(i, 1);
break;
}
}
}
};
signal.addEventListener("abort", removeListener);
}
}

Expand Down Expand Up @@ -924,15 +934,15 @@ class EventTarget {

// Per spec: Check if event is currently being dispatched
if (getDispatched(event)) {
throw new DOMException(
throw createDOMException(
"Failed to execute 'dispatchEvent' on 'EventTarget': The event is already being dispatched.",
"InvalidStateError",
);
}

// Per spec: Check if event's initialized flag is not set
if (event.eventPhase !== Event.NONE) {
throw new DOMException(
throw createDOMException(
"Failed to execute 'dispatchEvent' on 'EventTarget': The event's phase is not NONE.",
"InvalidStateError",
);
Expand Down Expand Up @@ -960,37 +970,6 @@ class EventTarget {
}
}

// Simple DOMException implementation for spec compliance
class DOMException extends Error {
override readonly name: string;
readonly code: number;

constructor(message?: string, name = "Error") {
super(message);
this.name = name;

// Common DOMException codes
const codes: { [key: string]: number; } = {
"InvalidStateError": 11,
"NotSupportedError": 9,
"InvalidCharacterError": 5,
"NoModificationAllowedError": 7,
"NotFoundError": 8,
"QuotaExceededError": 22,
"TypeMismatchError": 17,
"SecurityError": 18,
"NetworkError": 19,
"AbortError": 20,
"URLMismatchError": 21,
"InvalidAccessError": 15,
"ValidationError": 0,
"TimeoutError": 23,
};

this.code = codes[name] || 0;
}
}

class ErrorEvent extends Event {
readonly message: string;
readonly filename: string;
Expand Down Expand Up @@ -1224,3 +1203,171 @@ function reportError(error: any): void {
function listenerCount(target: any, type: string): number {
return getListeners(target)?.[type]?.length ?? 0;
}

// AbortSignal and AbortController implementation
// Compliant with WHATWG DOM Standard
// https://dom.spec.whatwg.org/#interface-abortsignal

// Private symbols for AbortSignal internal state
const _aborted = Symbol("[[aborted]]");
const _abortReason = Symbol("[[abortReason]]");
const _abortAlgorithms = Symbol("[[abortAlgorithms]]");

class AbortSignal extends EventTarget {
constructor() {
super();

(this as any)[_aborted] = false;
(this as any)[_abortReason] = undefined;
(this as any)[_abortAlgorithms] = new Set();
}
get aborted(): boolean {
return (this as any)[_aborted];
}

get reason(): any {
return (this as any)[_abortReason];
}

throwIfAborted(): void {
if ((this as any)[_aborted]) {
throw (this as any)[_abortReason];
}
}
// Static factory methods
static abort(reason?: any): AbortSignal {
const signal = new AbortSignal();
(signal as any)[_aborted] = true;
(signal as any)[_abortReason] = reason !== undefined ?
reason :
createDOMException("signal is aborted without reason", "AbortError");
return signal;
}
static timeout(milliseconds: number): AbortSignal {
if (milliseconds < 0) {
throw new RangeError("milliseconds must be non-negative");
}

const signal = new AbortSignal();
if (milliseconds === 0) {
(signal as any)[_aborted] = true;
(signal as any)[_abortReason] = createDOMException("signal timed out", "TimeoutError");
} else {
const timeoutCallback = function() {
if (!(signal as any)[_aborted]) {
signalAbort(
signal,
createDOMException("signal timed out", "TimeoutError"),
);
}
};
setTimeout(timeoutCallback, milliseconds);
}

return signal;
}
static any(signals: AbortSignal[]): AbortSignal {
const resultSignal = new AbortSignal();

// If any signal is already aborted, return an aborted signal
for (const signal of signals) {
if (signal.aborted) {
(resultSignal as any)[_aborted] = true;
(resultSignal as any)[_abortReason] = signal.reason;
return resultSignal;
}
}

// Listen for abort on any of the signals
for (const signal of signals) {
signal.addEventListener("abort", () => {
if (!resultSignal.aborted) {
signalAbort(resultSignal, signal.reason);
}
});
}

return resultSignal;
}
}

// AbortController implementation
class AbortController {
#signal: AbortSignal;

constructor() {
this.#signal = new AbortSignal();
}

get signal(): AbortSignal {
return this.#signal;
}

abort(reason?: any): void {
signalAbort(
this.#signal,
reason !== undefined ?
reason :
createDOMException("signal is aborted without reason", "AbortError"),
);
}
}

// Internal function to signal abort
function signalAbort(signal: AbortSignal, reason: any): void {
if ((signal as any)[_aborted]) {
return;
}

(signal as any)[_aborted] = true;
(signal as any)[_abortReason] = reason;

// Execute abort algorithms
const algorithms = (signal as any)[_abortAlgorithms];
for (const algorithm of algorithms) {
try {
algorithm();
} catch (error) {
// Report the exception but continue with other algorithms
reportError(error);
}
}
algorithms.clear();

// Fire abort event
const event = new Event("abort");
signal.dispatchEvent(event);
}

// DOMException implementation for abort-related errors
function createDOMException(message?: string, name: string = "Error"): Error {
const error = new Error(message);
error.name = name;

// Add code property for DOMException compatibility
const codes: Record<string, number> = {
"IndexSizeError": 1,
"HierarchyRequestError": 3,
"WrongDocumentError": 4,
"InvalidCharacterError": 5,
"NoModificationAllowedError": 7,
"NotFoundError": 8,
"NotSupportedError": 9,
"InvalidStateError": 11,
"SyntaxError": 12,
"InvalidModificationError": 13,
"NamespaceError": 14,
"InvalidAccessError": 15,
"SecurityError": 18,
"NetworkError": 19,
"AbortError": 20,
"URLMismatchError": 21,
"QuotaExceededError": 22,
"TimeoutError": 23,
"InvalidNodeTypeError": 24,
"DataCloneError": 25,
};

(error as any).code = codes[name] || 0;
return error;
}
Loading
Loading