Skip to content

Commit 11273a4

Browse files
committed
feat(ext/web): abort controller
1 parent 298d6fd commit 11273a4

File tree

4 files changed

+278
-46
lines changed

4 files changed

+278
-46
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@ anymap = "0.12.1"
1717
base64-simd = "0.8.0"
1818
clap = { version = "4.5.40", features = ["derive"] }
1919
clap_complete = "4.5.54"
20-
console = "0.15.8"
20+
console = "0.15.11"
2121
dprint-core = "0.67.4"
2222
dprint-plugin-typescript = "0.95.5"
2323
dprint-plugin-json = "0.20.0"
24-
indexmap = "2.0"
25-
image = "0.25.5"
24+
indexmap = "2.9.0"
25+
image = "0.25.6"
2626
libsui = "0.10.0"
27-
nova_vm = { git = "https://github.com/trynova/nova", rev = "2f09304d915a3157e73ffc4a7f48bbd34aad3844", features = [
27+
nova_vm = { git = "https://github.com/trynova/nova", rev = "7d1da0bd906ab7b8409878b41bd611f172789ce9", features = [
2828
"typescript"
2929
] }
30-
nu-ansi-term = "0.50.0"
30+
nu-ansi-term = "0.50.1"
3131
owo-colors = "4.2.1"
3232
oxc_ast = "0.72.0"
3333
oxc_allocator = "0.72.0"
@@ -40,10 +40,10 @@ rand = "0.9.1"
4040
reedline = "0.40.0"
4141
regex = "1.11.1"
4242
ring = "0.17.8"
43-
serde = { version = "1.0.130", features = ["derive"] }
43+
serde = { version = "1.0.219", features = ["derive"] }
4444
thiserror = "2.0.12"
4545
tokio = { version = "1.45.1", features = ["rt", "sync", "time"] }
46-
url = { version = "2", features = ["serde", "expose_internals"] }
46+
url = { version = "2.5.4", features = ["serde", "expose_internals"] }
4747
wgpu = { version = "25.0.2", features = ["wgsl", "webgpu"] }
4848

4949
[profile.release]

runtime/src/ext/web/event.ts

Lines changed: 184 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -859,15 +859,25 @@ class EventTarget {
859859
return;
860860
}
861861
}
862-
863862
if (processedOptions?.signal) {
864863
const signal = processedOptions?.signal;
865864
if (signal.aborted) {
866865
return;
867866
} else {
868-
signal.addEventListener("abort", () => {
869-
this.removeEventListener(type, callback, options);
870-
});
867+
const removeListener = () => {
868+
// Remove the specific listener entry that was added
869+
const listenerList = this[eventTargetData].listeners[type];
870+
if (listenerList) {
871+
for (let i = 0; i < listenerList.length; i++) {
872+
const listener = listenerList[i];
873+
if (listener.callback === callback && listener.options === processedOptions) {
874+
listenerList.splice(i, 1);
875+
break;
876+
}
877+
}
878+
}
879+
};
880+
signal.addEventListener("abort", removeListener);
871881
}
872882
}
873883

@@ -924,15 +934,15 @@ class EventTarget {
924934

925935
// Per spec: Check if event is currently being dispatched
926936
if (getDispatched(event)) {
927-
throw new DOMException(
937+
throw createDOMException(
928938
"Failed to execute 'dispatchEvent' on 'EventTarget': The event is already being dispatched.",
929939
"InvalidStateError",
930940
);
931941
}
932942

933943
// Per spec: Check if event's initialized flag is not set
934944
if (event.eventPhase !== Event.NONE) {
935-
throw new DOMException(
945+
throw createDOMException(
936946
"Failed to execute 'dispatchEvent' on 'EventTarget': The event's phase is not NONE.",
937947
"InvalidStateError",
938948
);
@@ -960,37 +970,6 @@ class EventTarget {
960970
}
961971
}
962972

963-
// Simple DOMException implementation for spec compliance
964-
class DOMException extends Error {
965-
override readonly name: string;
966-
readonly code: number;
967-
968-
constructor(message?: string, name = "Error") {
969-
super(message);
970-
this.name = name;
971-
972-
// Common DOMException codes
973-
const codes: { [key: string]: number; } = {
974-
"InvalidStateError": 11,
975-
"NotSupportedError": 9,
976-
"InvalidCharacterError": 5,
977-
"NoModificationAllowedError": 7,
978-
"NotFoundError": 8,
979-
"QuotaExceededError": 22,
980-
"TypeMismatchError": 17,
981-
"SecurityError": 18,
982-
"NetworkError": 19,
983-
"AbortError": 20,
984-
"URLMismatchError": 21,
985-
"InvalidAccessError": 15,
986-
"ValidationError": 0,
987-
"TimeoutError": 23,
988-
};
989-
990-
this.code = codes[name] || 0;
991-
}
992-
}
993-
994973
class ErrorEvent extends Event {
995974
readonly message: string;
996975
readonly filename: string;
@@ -1224,3 +1203,171 @@ function reportError(error: any): void {
12241203
function listenerCount(target: any, type: string): number {
12251204
return getListeners(target)?.[type]?.length ?? 0;
12261205
}
1206+
1207+
// AbortSignal and AbortController implementation
1208+
// Compliant with WHATWG DOM Standard
1209+
// https://dom.spec.whatwg.org/#interface-abortsignal
1210+
1211+
// Private symbols for AbortSignal internal state
1212+
const _aborted = Symbol("[[aborted]]");
1213+
const _abortReason = Symbol("[[abortReason]]");
1214+
const _abortAlgorithms = Symbol("[[abortAlgorithms]]");
1215+
1216+
class AbortSignal extends EventTarget {
1217+
constructor() {
1218+
super();
1219+
1220+
(this as any)[_aborted] = false;
1221+
(this as any)[_abortReason] = undefined;
1222+
(this as any)[_abortAlgorithms] = new Set();
1223+
}
1224+
get aborted(): boolean {
1225+
return (this as any)[_aborted];
1226+
}
1227+
1228+
get reason(): any {
1229+
return (this as any)[_abortReason];
1230+
}
1231+
1232+
throwIfAborted(): void {
1233+
if ((this as any)[_aborted]) {
1234+
throw (this as any)[_abortReason];
1235+
}
1236+
}
1237+
// Static factory methods
1238+
static abort(reason?: any): AbortSignal {
1239+
const signal = new AbortSignal();
1240+
(signal as any)[_aborted] = true;
1241+
(signal as any)[_abortReason] = reason !== undefined ?
1242+
reason :
1243+
createDOMException("signal is aborted without reason", "AbortError");
1244+
return signal;
1245+
}
1246+
static timeout(milliseconds: number): AbortSignal {
1247+
if (milliseconds < 0) {
1248+
throw new RangeError("milliseconds must be non-negative");
1249+
}
1250+
1251+
const signal = new AbortSignal();
1252+
if (milliseconds === 0) {
1253+
(signal as any)[_aborted] = true;
1254+
(signal as any)[_abortReason] = createDOMException("signal timed out", "TimeoutError");
1255+
} else {
1256+
const timeoutCallback = function() {
1257+
if (!(signal as any)[_aborted]) {
1258+
signalAbort(
1259+
signal,
1260+
createDOMException("signal timed out", "TimeoutError"),
1261+
);
1262+
}
1263+
};
1264+
setTimeout(timeoutCallback, milliseconds);
1265+
}
1266+
1267+
return signal;
1268+
}
1269+
static any(signals: AbortSignal[]): AbortSignal {
1270+
const resultSignal = new AbortSignal();
1271+
1272+
// If any signal is already aborted, return an aborted signal
1273+
for (const signal of signals) {
1274+
if (signal.aborted) {
1275+
(resultSignal as any)[_aborted] = true;
1276+
(resultSignal as any)[_abortReason] = signal.reason;
1277+
return resultSignal;
1278+
}
1279+
}
1280+
1281+
// Listen for abort on any of the signals
1282+
for (const signal of signals) {
1283+
signal.addEventListener("abort", () => {
1284+
if (!resultSignal.aborted) {
1285+
signalAbort(resultSignal, signal.reason);
1286+
}
1287+
});
1288+
}
1289+
1290+
return resultSignal;
1291+
}
1292+
}
1293+
1294+
// AbortController implementation
1295+
class AbortController {
1296+
#signal: AbortSignal;
1297+
1298+
constructor() {
1299+
this.#signal = new AbortSignal();
1300+
}
1301+
1302+
get signal(): AbortSignal {
1303+
return this.#signal;
1304+
}
1305+
1306+
abort(reason?: any): void {
1307+
signalAbort(
1308+
this.#signal,
1309+
reason !== undefined ?
1310+
reason :
1311+
createDOMException("signal is aborted without reason", "AbortError"),
1312+
);
1313+
}
1314+
}
1315+
1316+
// Internal function to signal abort
1317+
function signalAbort(signal: AbortSignal, reason: any): void {
1318+
if ((signal as any)[_aborted]) {
1319+
return;
1320+
}
1321+
1322+
(signal as any)[_aborted] = true;
1323+
(signal as any)[_abortReason] = reason;
1324+
1325+
// Execute abort algorithms
1326+
const algorithms = (signal as any)[_abortAlgorithms];
1327+
for (const algorithm of algorithms) {
1328+
try {
1329+
algorithm();
1330+
} catch (error) {
1331+
// Report the exception but continue with other algorithms
1332+
reportError(error);
1333+
}
1334+
}
1335+
algorithms.clear();
1336+
1337+
// Fire abort event
1338+
const event = new Event("abort");
1339+
signal.dispatchEvent(event);
1340+
}
1341+
1342+
// DOMException implementation for abort-related errors
1343+
function createDOMException(message?: string, name: string = "Error"): Error {
1344+
const error = new Error(message);
1345+
error.name = name;
1346+
1347+
// Add code property for DOMException compatibility
1348+
const codes: Record<string, number> = {
1349+
"IndexSizeError": 1,
1350+
"HierarchyRequestError": 3,
1351+
"WrongDocumentError": 4,
1352+
"InvalidCharacterError": 5,
1353+
"NoModificationAllowedError": 7,
1354+
"NotFoundError": 8,
1355+
"NotSupportedError": 9,
1356+
"InvalidStateError": 11,
1357+
"SyntaxError": 12,
1358+
"InvalidModificationError": 13,
1359+
"NamespaceError": 14,
1360+
"InvalidAccessError": 15,
1361+
"SecurityError": 18,
1362+
"NetworkError": 19,
1363+
"AbortError": 20,
1364+
"URLMismatchError": 21,
1365+
"QuotaExceededError": 22,
1366+
"TimeoutError": 23,
1367+
"InvalidNodeTypeError": 24,
1368+
"DataCloneError": 25,
1369+
};
1370+
1371+
(error as any).code = codes[name] || 0;
1372+
return error;
1373+
}

0 commit comments

Comments
 (0)