@evolu/react-native 16.0.0 → 16.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/LockManager.d.ts.map +1 -1
- package/dist/src/LockManager.js +2 -0
- package/dist/src/Polyfills.d.ts.map +1 -1
- package/dist/src/Polyfills.js +7 -2
- package/dist/src/Task.d.ts +2 -1
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +5 -2
- package/dist/src/components/EvoluIdenticon.js +2 -2
- package/dist/src/sqlite-drivers/createExpoSqliteDriver.d.ts.map +1 -1
- package/dist/src/sqlite-drivers/createExpoSqliteDriver.js +1 -0
- package/package.json +8 -6
- package/src/ErrorUtils.d.ts +2 -1
- package/src/LockManager.test.ts +537 -0
- package/src/LockManager.ts +2 -0
- package/src/Polyfills.test.ts +623 -0
- package/src/Polyfills.ts +7 -2
- package/src/Task.test.ts +82 -0
- package/src/Task.ts +8 -3
- package/src/components/EvoluIdenticon.tsx +2 -2
- package/src/shared.ts +1 -1
- package/src/sqlite-drivers/createExpoSqliteDriver.ts +5 -1
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertEqual,
|
|
3
|
+
assertFalse,
|
|
4
|
+
assertInstanceOf,
|
|
5
|
+
assertRejectsSame,
|
|
6
|
+
assertSame,
|
|
7
|
+
assertTrue,
|
|
8
|
+
} from "@evolu/common";
|
|
9
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
10
|
+
import { installPolyfills } from "./Polyfills.ts";
|
|
11
|
+
|
|
12
|
+
interface GlobalAbort {
|
|
13
|
+
readonly AbortController: typeof AbortController | undefined;
|
|
14
|
+
readonly AbortSignal: typeof AbortSignal | undefined;
|
|
15
|
+
readonly DOMException: typeof DOMException | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PromiseStatics {
|
|
19
|
+
withResolvers?: () => {
|
|
20
|
+
promise: Promise<unknown>;
|
|
21
|
+
resolve: (value: unknown) => void;
|
|
22
|
+
reject: (reason?: unknown) => void;
|
|
23
|
+
};
|
|
24
|
+
try?: (
|
|
25
|
+
func: (...args: ReadonlyArray<unknown>) => unknown,
|
|
26
|
+
...args: ReadonlyArray<unknown>
|
|
27
|
+
) => Promise<unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ArrayConstructorWithFromAsync {
|
|
31
|
+
fromAsync?: typeof Array.fromAsync;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type FakeAbortListener = () => void;
|
|
35
|
+
|
|
36
|
+
interface FakeAbortSignal {
|
|
37
|
+
aborted: boolean;
|
|
38
|
+
addEventListener: (
|
|
39
|
+
type: string,
|
|
40
|
+
listener: FakeAbortListener,
|
|
41
|
+
options?: { readonly once?: boolean },
|
|
42
|
+
) => void;
|
|
43
|
+
removeEventListener: (type: string, listener: FakeAbortListener) => void;
|
|
44
|
+
dispatchAbort: () => void;
|
|
45
|
+
getListenerCount: () => number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const createFakeAbortRuntime = (): {
|
|
49
|
+
readonly AbortSignal: typeof AbortSignal;
|
|
50
|
+
readonly AbortController: typeof AbortController;
|
|
51
|
+
readonly getAbortCallCount: () => number;
|
|
52
|
+
} => {
|
|
53
|
+
let abortCallCount = 0;
|
|
54
|
+
|
|
55
|
+
class TestAbortSignal {
|
|
56
|
+
public aborted = false;
|
|
57
|
+
private readonly listeners = new Set<FakeAbortListener>();
|
|
58
|
+
|
|
59
|
+
public addEventListener(
|
|
60
|
+
_type: string,
|
|
61
|
+
listener: FakeAbortListener,
|
|
62
|
+
_options?: { readonly once?: boolean },
|
|
63
|
+
): void {
|
|
64
|
+
this.listeners.add(listener);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public removeEventListener(
|
|
68
|
+
_type: string,
|
|
69
|
+
listener: FakeAbortListener,
|
|
70
|
+
): void {
|
|
71
|
+
this.listeners.delete(listener);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public dispatchAbort(): void {
|
|
75
|
+
if (this.aborted) return;
|
|
76
|
+
this.aborted = true;
|
|
77
|
+
for (const listener of this.listeners) listener();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public getListenerCount(): number {
|
|
81
|
+
return this.listeners.size;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class TestAbortController {
|
|
86
|
+
public readonly signal =
|
|
87
|
+
new TestAbortSignal() as unknown as AbortController["signal"];
|
|
88
|
+
|
|
89
|
+
public abort(): void {
|
|
90
|
+
abortCallCount += 1;
|
|
91
|
+
(this.signal as unknown as FakeAbortSignal).dispatchAbort();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
AbortController: TestAbortController,
|
|
97
|
+
AbortSignal: TestAbortSignal as unknown as typeof AbortSignal,
|
|
98
|
+
getAbortCallCount: () => abortCallCount,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const setAbortGlobals = (globals: GlobalAbort): void => {
|
|
103
|
+
if (globals.AbortController === undefined) {
|
|
104
|
+
delete (globalThis as { AbortController?: typeof AbortController })
|
|
105
|
+
.AbortController;
|
|
106
|
+
} else {
|
|
107
|
+
(
|
|
108
|
+
globalThis as { AbortController?: typeof AbortController }
|
|
109
|
+
).AbortController = globals.AbortController;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (globals.AbortSignal === undefined) {
|
|
113
|
+
delete (globalThis as { AbortSignal?: typeof AbortSignal }).AbortSignal;
|
|
114
|
+
} else {
|
|
115
|
+
(globalThis as { AbortSignal?: typeof AbortSignal }).AbortSignal =
|
|
116
|
+
globals.AbortSignal;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (globals.DOMException === undefined) {
|
|
120
|
+
delete (globalThis as { DOMException?: typeof DOMException }).DOMException;
|
|
121
|
+
} else {
|
|
122
|
+
(globalThis as { DOMException?: typeof DOMException }).DOMException =
|
|
123
|
+
globals.DOMException;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
describe("installPolyfills", () => {
|
|
128
|
+
let originalGlobals: GlobalAbort;
|
|
129
|
+
let originalArrayFromAsync: PropertyDescriptor | undefined;
|
|
130
|
+
let originalPromiseWithResolvers: PropertyDescriptor | undefined;
|
|
131
|
+
let originalPromiseTry: PropertyDescriptor | undefined;
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
originalGlobals = {
|
|
135
|
+
AbortController: AbortController,
|
|
136
|
+
AbortSignal: AbortSignal,
|
|
137
|
+
DOMException: DOMException,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
originalArrayFromAsync = Object.getOwnPropertyDescriptor(
|
|
141
|
+
Array,
|
|
142
|
+
"fromAsync",
|
|
143
|
+
);
|
|
144
|
+
originalPromiseWithResolvers = Object.getOwnPropertyDescriptor(
|
|
145
|
+
Promise,
|
|
146
|
+
"withResolvers",
|
|
147
|
+
);
|
|
148
|
+
originalPromiseTry = Object.getOwnPropertyDescriptor(Promise, "try");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
afterEach(() => {
|
|
152
|
+
setAbortGlobals(originalGlobals);
|
|
153
|
+
|
|
154
|
+
if (originalArrayFromAsync === undefined) {
|
|
155
|
+
delete (Array as ArrayConstructorWithFromAsync).fromAsync;
|
|
156
|
+
} else {
|
|
157
|
+
Object.defineProperty(Array, "fromAsync", originalArrayFromAsync);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (originalPromiseWithResolvers === undefined) {
|
|
161
|
+
delete (Promise as PromiseStatics).withResolvers;
|
|
162
|
+
} else {
|
|
163
|
+
Object.defineProperty(
|
|
164
|
+
Promise,
|
|
165
|
+
"withResolvers",
|
|
166
|
+
originalPromiseWithResolvers,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (originalPromiseTry === undefined) {
|
|
171
|
+
delete (Promise as PromiseStatics).try;
|
|
172
|
+
} else {
|
|
173
|
+
Object.defineProperty(Promise, "try", originalPromiseTry);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("polyfills Array.fromAsync", async () => {
|
|
178
|
+
delete (Array as ArrayConstructorWithFromAsync).fromAsync;
|
|
179
|
+
|
|
180
|
+
installPolyfills();
|
|
181
|
+
|
|
182
|
+
assertEqual(await Array.fromAsync(new Set([1, 2, 3])), [1, 2, 3]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("polyfills Promise.withResolvers", async () => {
|
|
186
|
+
delete (Promise as PromiseStatics).withResolvers;
|
|
187
|
+
|
|
188
|
+
installPolyfills();
|
|
189
|
+
|
|
190
|
+
const PromiseStatic = Promise as PromiseStatics;
|
|
191
|
+
assertEqual(typeof PromiseStatic.withResolvers, "function");
|
|
192
|
+
|
|
193
|
+
const { promise, resolve } = PromiseStatic.withResolvers!();
|
|
194
|
+
resolve("ok");
|
|
195
|
+
|
|
196
|
+
assertEqual(await promise, "ok");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("polyfills Promise.try and forwards arguments", async () => {
|
|
200
|
+
delete (Promise as PromiseStatics).try;
|
|
201
|
+
|
|
202
|
+
installPolyfills();
|
|
203
|
+
|
|
204
|
+
const PromiseStatic = Promise as PromiseStatics;
|
|
205
|
+
assertEqual(typeof PromiseStatic.try, "function");
|
|
206
|
+
|
|
207
|
+
const result = await PromiseStatic.try!(
|
|
208
|
+
(a, b) => `${String(a)}-${String(b)}`,
|
|
209
|
+
"a",
|
|
210
|
+
1,
|
|
211
|
+
);
|
|
212
|
+
assertEqual(result, "a-1");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("Promise.try rejects when callback throws", async () => {
|
|
216
|
+
delete (Promise as PromiseStatics).try;
|
|
217
|
+
|
|
218
|
+
installPolyfills();
|
|
219
|
+
|
|
220
|
+
const PromiseStatic = Promise as PromiseStatics;
|
|
221
|
+
const error = new Error("boom");
|
|
222
|
+
|
|
223
|
+
await assertRejectsSame(
|
|
224
|
+
PromiseStatic.try!(() => {
|
|
225
|
+
throw error;
|
|
226
|
+
}),
|
|
227
|
+
error,
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("does not override existing Promise static methods", () => {
|
|
232
|
+
const withResolvers = () => {
|
|
233
|
+
const promise = Promise.resolve("existing");
|
|
234
|
+
return {
|
|
235
|
+
promise,
|
|
236
|
+
resolve: () => undefined,
|
|
237
|
+
reject: () => undefined,
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
const promiseTry = () => Promise.resolve("existing");
|
|
241
|
+
|
|
242
|
+
(Promise as PromiseStatics).withResolvers = withResolvers;
|
|
243
|
+
(Promise as PromiseStatics).try = promiseTry;
|
|
244
|
+
|
|
245
|
+
installPolyfills();
|
|
246
|
+
|
|
247
|
+
const PromiseStatic = Promise as PromiseStatics;
|
|
248
|
+
assertSame(PromiseStatic.withResolvers, withResolvers);
|
|
249
|
+
assertSame(PromiseStatic.try, promiseTry);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("polyfills reason propagation", () => {
|
|
253
|
+
const runtime = createFakeAbortRuntime();
|
|
254
|
+
setAbortGlobals({ ...runtime, DOMException: DOMException });
|
|
255
|
+
|
|
256
|
+
installPolyfills();
|
|
257
|
+
|
|
258
|
+
const controller = new AbortController();
|
|
259
|
+
const reason = new Error("stop");
|
|
260
|
+
controller.abort(reason);
|
|
261
|
+
|
|
262
|
+
assertSame(
|
|
263
|
+
(controller.signal as { readonly reason: unknown }).reason,
|
|
264
|
+
reason,
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("creates AbortError reason when none is provided", () => {
|
|
269
|
+
const runtime = createFakeAbortRuntime();
|
|
270
|
+
setAbortGlobals({ ...runtime, DOMException: undefined });
|
|
271
|
+
|
|
272
|
+
installPolyfills();
|
|
273
|
+
|
|
274
|
+
const controller = new AbortController();
|
|
275
|
+
controller.abort();
|
|
276
|
+
|
|
277
|
+
const reason = (controller.signal as { readonly reason: Error }).reason;
|
|
278
|
+
assertEqual(reason.name, "AbortError");
|
|
279
|
+
assertEqual(reason.message, "This operation was aborted");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("polyfills AbortSignal.throwIfAborted", () => {
|
|
283
|
+
const runtime = createFakeAbortRuntime();
|
|
284
|
+
setAbortGlobals({ ...runtime, DOMException: DOMException });
|
|
285
|
+
|
|
286
|
+
installPolyfills();
|
|
287
|
+
|
|
288
|
+
const controller = new AbortController();
|
|
289
|
+
controller.signal.throwIfAborted();
|
|
290
|
+
|
|
291
|
+
const reason = new Error("stop");
|
|
292
|
+
controller.abort(reason);
|
|
293
|
+
|
|
294
|
+
let thrown: unknown;
|
|
295
|
+
try {
|
|
296
|
+
controller.signal.throwIfAborted();
|
|
297
|
+
} catch (error) {
|
|
298
|
+
thrown = error;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
assertSame(thrown, reason);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("does not override AbortSignal.throwIfAborted", () => {
|
|
305
|
+
const runtime = createFakeAbortRuntime();
|
|
306
|
+
let called = false;
|
|
307
|
+
const throwIfAborted = () => {
|
|
308
|
+
called = true;
|
|
309
|
+
};
|
|
310
|
+
Object.defineProperty(runtime.AbortSignal.prototype, "throwIfAborted", {
|
|
311
|
+
value: throwIfAborted,
|
|
312
|
+
});
|
|
313
|
+
setAbortGlobals({ ...runtime, DOMException: DOMException });
|
|
314
|
+
|
|
315
|
+
installPolyfills();
|
|
316
|
+
|
|
317
|
+
new AbortController().signal.throwIfAborted();
|
|
318
|
+
|
|
319
|
+
assertTrue(called);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("polyfills AbortSignal.abort", () => {
|
|
323
|
+
const runtime = createFakeAbortRuntime();
|
|
324
|
+
setAbortGlobals({ ...runtime, DOMException: DOMException });
|
|
325
|
+
|
|
326
|
+
installPolyfills();
|
|
327
|
+
|
|
328
|
+
const signal = (
|
|
329
|
+
AbortSignal as typeof AbortSignal & {
|
|
330
|
+
abort: (reason?: unknown) => AbortSignal;
|
|
331
|
+
}
|
|
332
|
+
).abort("manual");
|
|
333
|
+
|
|
334
|
+
assertTrue(signal.aborted);
|
|
335
|
+
assertEqual((signal as { readonly reason: unknown }).reason, "manual");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("polyfills AbortSignal.timeout without DOMException", async () => {
|
|
339
|
+
const runtime = createFakeAbortRuntime();
|
|
340
|
+
setAbortGlobals({ ...runtime, DOMException: undefined });
|
|
341
|
+
|
|
342
|
+
installPolyfills();
|
|
343
|
+
|
|
344
|
+
const signal = (
|
|
345
|
+
AbortSignal as typeof AbortSignal & {
|
|
346
|
+
timeout: (milliseconds: number) => AbortSignal;
|
|
347
|
+
}
|
|
348
|
+
).timeout(1);
|
|
349
|
+
|
|
350
|
+
await new Promise((resolve) => {
|
|
351
|
+
setTimeout(resolve, 5);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const reason = (signal as { readonly reason: Error }).reason;
|
|
355
|
+
assertTrue(signal.aborted);
|
|
356
|
+
assertEqual(reason.name, "TimeoutError");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("polyfills AbortSignal.any with first aborted reason", () => {
|
|
360
|
+
const runtime = createFakeAbortRuntime();
|
|
361
|
+
setAbortGlobals({ ...runtime, DOMException: DOMException });
|
|
362
|
+
|
|
363
|
+
installPolyfills();
|
|
364
|
+
|
|
365
|
+
const controller1 = new AbortController();
|
|
366
|
+
const controller2 = new AbortController();
|
|
367
|
+
const signal = (
|
|
368
|
+
AbortSignal as typeof AbortSignal & {
|
|
369
|
+
any: (signals: ReadonlyArray<AbortSignal>) => AbortSignal;
|
|
370
|
+
}
|
|
371
|
+
).any([controller1.signal, controller2.signal]);
|
|
372
|
+
|
|
373
|
+
const reason = new Error("cancelled");
|
|
374
|
+
controller2.abort(reason);
|
|
375
|
+
|
|
376
|
+
assertTrue(signal.aborted);
|
|
377
|
+
assertSame((signal as { readonly reason: unknown }).reason, reason);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("is idempotent and does not re-patch abort", () => {
|
|
381
|
+
const runtime = createFakeAbortRuntime();
|
|
382
|
+
setAbortGlobals({ ...runtime, DOMException: DOMException });
|
|
383
|
+
|
|
384
|
+
installPolyfills();
|
|
385
|
+
installPolyfills();
|
|
386
|
+
|
|
387
|
+
const controller = new AbortController();
|
|
388
|
+
controller.abort(new Error("stop"));
|
|
389
|
+
|
|
390
|
+
assertEqual(runtime.getAbortCallCount(), 1);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("does not override existing AbortSignal static methods", () => {
|
|
394
|
+
const runtime = createFakeAbortRuntime();
|
|
395
|
+
const abort = () => ({ aborted: true }) as AbortSignal;
|
|
396
|
+
const timeout = () => ({ aborted: false }) as AbortSignal;
|
|
397
|
+
const any = () => ({ aborted: false }) as AbortSignal;
|
|
398
|
+
|
|
399
|
+
Object.assign(runtime.AbortSignal, { abort, timeout, any });
|
|
400
|
+
setAbortGlobals({ ...runtime, DOMException: DOMException });
|
|
401
|
+
|
|
402
|
+
installPolyfills();
|
|
403
|
+
|
|
404
|
+
const AbortSignalStatic = AbortSignal as typeof AbortSignal & {
|
|
405
|
+
abort: typeof abort;
|
|
406
|
+
timeout: typeof timeout;
|
|
407
|
+
any: typeof any;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const descriptors = Object.getOwnPropertyDescriptors(AbortSignalStatic);
|
|
411
|
+
assertSame(descriptors.abort.value, abort);
|
|
412
|
+
assertSame(descriptors.timeout.value, timeout);
|
|
413
|
+
assertSame(descriptors.any.value, any);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("AbortSignal.any handles empty input", () => {
|
|
417
|
+
const runtime = createFakeAbortRuntime();
|
|
418
|
+
setAbortGlobals({ ...runtime, DOMException: DOMException });
|
|
419
|
+
|
|
420
|
+
installPolyfills();
|
|
421
|
+
|
|
422
|
+
const signal = (
|
|
423
|
+
AbortSignal as typeof AbortSignal & {
|
|
424
|
+
any: (signals: ReadonlyArray<AbortSignal>) => AbortSignal;
|
|
425
|
+
}
|
|
426
|
+
).any([]);
|
|
427
|
+
|
|
428
|
+
assertFalse(signal.aborted);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("AbortSignal.any dedupes duplicate source signals", () => {
|
|
432
|
+
const runtime = createFakeAbortRuntime();
|
|
433
|
+
setAbortGlobals({ ...runtime, DOMException: DOMException });
|
|
434
|
+
|
|
435
|
+
installPolyfills();
|
|
436
|
+
|
|
437
|
+
const sourceController = new AbortController();
|
|
438
|
+
const sourceSignal = sourceController.signal as unknown as FakeAbortSignal;
|
|
439
|
+
|
|
440
|
+
const signal = (
|
|
441
|
+
AbortSignal as typeof AbortSignal & {
|
|
442
|
+
any: (signals: ReadonlyArray<AbortSignal>) => AbortSignal;
|
|
443
|
+
}
|
|
444
|
+
).any([
|
|
445
|
+
sourceController.signal,
|
|
446
|
+
sourceController.signal,
|
|
447
|
+
sourceController.signal,
|
|
448
|
+
]);
|
|
449
|
+
|
|
450
|
+
assertTrue(sourceSignal.getListenerCount() <= 1);
|
|
451
|
+
|
|
452
|
+
const reason = new Error("duplicate-source");
|
|
453
|
+
sourceController.abort(reason);
|
|
454
|
+
|
|
455
|
+
assertTrue(signal.aborted);
|
|
456
|
+
assertSame((signal as { readonly reason: unknown }).reason, reason);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("AbortSignal.any uses first already-aborted signal in input order", () => {
|
|
460
|
+
const runtime = createFakeAbortRuntime();
|
|
461
|
+
setAbortGlobals({ ...runtime, DOMException: DOMException });
|
|
462
|
+
|
|
463
|
+
installPolyfills();
|
|
464
|
+
|
|
465
|
+
const firstController = new AbortController();
|
|
466
|
+
const secondController = new AbortController();
|
|
467
|
+
|
|
468
|
+
const firstReason = new Error("first");
|
|
469
|
+
const secondReason = new Error("second");
|
|
470
|
+
firstController.abort(firstReason);
|
|
471
|
+
secondController.abort(secondReason);
|
|
472
|
+
|
|
473
|
+
const signal = (
|
|
474
|
+
AbortSignal as typeof AbortSignal & {
|
|
475
|
+
any: (signals: ReadonlyArray<AbortSignal>) => AbortSignal;
|
|
476
|
+
}
|
|
477
|
+
).any([firstController.signal, secondController.signal]);
|
|
478
|
+
|
|
479
|
+
assertTrue(signal.aborted);
|
|
480
|
+
assertSame((signal as { readonly reason: unknown }).reason, firstReason);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("AbortSignal.any uses AbortError when aborted signal has no reason", () => {
|
|
484
|
+
const runtime = createFakeAbortRuntime();
|
|
485
|
+
setAbortGlobals({ ...runtime, DOMException: undefined });
|
|
486
|
+
|
|
487
|
+
installPolyfills();
|
|
488
|
+
|
|
489
|
+
const signalWithoutReason = {
|
|
490
|
+
aborted: true,
|
|
491
|
+
addEventListener: () => undefined,
|
|
492
|
+
removeEventListener: () => undefined,
|
|
493
|
+
} as unknown as AbortSignal;
|
|
494
|
+
|
|
495
|
+
const signal = (
|
|
496
|
+
AbortSignal as typeof AbortSignal & {
|
|
497
|
+
any: (signals: ReadonlyArray<AbortSignal>) => AbortSignal;
|
|
498
|
+
}
|
|
499
|
+
).any([signalWithoutReason]);
|
|
500
|
+
|
|
501
|
+
const reason = (signal as { readonly reason: Error }).reason;
|
|
502
|
+
assertEqual(reason.name, "AbortError");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("falls back to Error when DOMException constructor throws", async () => {
|
|
506
|
+
const runtime = createFakeAbortRuntime();
|
|
507
|
+
const throwingDomException = function () {
|
|
508
|
+
throw new Error("broken DOMException");
|
|
509
|
+
} as unknown as typeof DOMException;
|
|
510
|
+
|
|
511
|
+
setAbortGlobals({ ...runtime, DOMException: throwingDomException });
|
|
512
|
+
|
|
513
|
+
installPolyfills();
|
|
514
|
+
|
|
515
|
+
const signal = (
|
|
516
|
+
AbortSignal as typeof AbortSignal & {
|
|
517
|
+
timeout: (milliseconds: number) => AbortSignal;
|
|
518
|
+
}
|
|
519
|
+
).timeout(1);
|
|
520
|
+
|
|
521
|
+
await new Promise((resolve) => {
|
|
522
|
+
setTimeout(resolve, 5);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const reason = (signal as { readonly reason: Error }).reason;
|
|
526
|
+
assertInstanceOf(reason, Error);
|
|
527
|
+
assertEqual(reason.name, "TimeoutError");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("AbortSignal.any does not add unbounded listeners to a long-lived source", () => {
|
|
531
|
+
const runtime = createFakeAbortRuntime();
|
|
532
|
+
setAbortGlobals({ ...runtime, DOMException: DOMException });
|
|
533
|
+
|
|
534
|
+
installPolyfills();
|
|
535
|
+
|
|
536
|
+
const sourceController = new AbortController();
|
|
537
|
+
const sourceSignal = sourceController.signal as unknown as FakeAbortSignal;
|
|
538
|
+
|
|
539
|
+
const AbortSignalStatic = AbortSignal as typeof AbortSignal & {
|
|
540
|
+
any: (signals: ReadonlyArray<AbortSignal>) => AbortSignal;
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
for (let i = 0; i < 100; i += 1) {
|
|
544
|
+
AbortSignalStatic.any([sourceController.signal]);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
assertTrue(sourceSignal.getListenerCount() <= 1);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("AbortSignal.any tolerates stale aborted references", () => {
|
|
551
|
+
const runtime = createFakeAbortRuntime();
|
|
552
|
+
setAbortGlobals({ ...runtime, DOMException: DOMException });
|
|
553
|
+
|
|
554
|
+
const originalWeakRef = WeakRef;
|
|
555
|
+
let callCount = 0;
|
|
556
|
+
|
|
557
|
+
(globalThis as { WeakRef?: unknown }).WeakRef = function (
|
|
558
|
+
this: unknown,
|
|
559
|
+
controller: AbortController,
|
|
560
|
+
) {
|
|
561
|
+
const preAbortedController = new AbortController();
|
|
562
|
+
preAbortedController.abort("already-done");
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
deref: () => {
|
|
566
|
+
callCount += 1;
|
|
567
|
+
return callCount === 1 ? controller : preAbortedController;
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
installPolyfills();
|
|
574
|
+
|
|
575
|
+
const sourceController = new AbortController();
|
|
576
|
+
(
|
|
577
|
+
AbortSignal as typeof AbortSignal & {
|
|
578
|
+
any: (signals: ReadonlyArray<AbortSignal>) => AbortSignal;
|
|
579
|
+
}
|
|
580
|
+
).any([sourceController.signal]);
|
|
581
|
+
|
|
582
|
+
sourceController.abort("ignored");
|
|
583
|
+
} finally {
|
|
584
|
+
(globalThis as { WeakRef?: unknown }).WeakRef = originalWeakRef;
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("AbortSignal.any tolerates cleared weak refs", () => {
|
|
589
|
+
const runtime = createFakeAbortRuntime();
|
|
590
|
+
setAbortGlobals({ ...runtime, DOMException: DOMException });
|
|
591
|
+
|
|
592
|
+
const originalWeakRef = WeakRef;
|
|
593
|
+
|
|
594
|
+
let callCount = 0;
|
|
595
|
+
|
|
596
|
+
(globalThis as { WeakRef?: unknown }).WeakRef = function (
|
|
597
|
+
this: unknown,
|
|
598
|
+
controller: AbortController,
|
|
599
|
+
) {
|
|
600
|
+
return {
|
|
601
|
+
deref: () => {
|
|
602
|
+
callCount += 1;
|
|
603
|
+
return callCount === 1 ? controller : undefined;
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
installPolyfills();
|
|
610
|
+
|
|
611
|
+
const sourceController = new AbortController();
|
|
612
|
+
(
|
|
613
|
+
AbortSignal as typeof AbortSignal & {
|
|
614
|
+
any: (signals: ReadonlyArray<AbortSignal>) => AbortSignal;
|
|
615
|
+
}
|
|
616
|
+
).any([sourceController.signal]);
|
|
617
|
+
|
|
618
|
+
sourceController.abort("ignored");
|
|
619
|
+
} finally {
|
|
620
|
+
(globalThis as { WeakRef?: unknown }).WeakRef = originalWeakRef;
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
});
|
package/src/Polyfills.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
/*
|
|
2
|
-
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
1
|
+
/* oxlint-disable typescript/no-unsafe-call, typescript/no-unsafe-member-access */
|
|
3
2
|
|
|
4
3
|
import { installPolyfills as installCommonPolyfills } from "@evolu/common/polyfills";
|
|
5
4
|
// @ts-expect-error Runtime polyfill package has no TypeScript declarations.
|
|
@@ -93,6 +92,7 @@ const anyControllersBySignal = new WeakMap<
|
|
|
93
92
|
AnyControllersRegistry
|
|
94
93
|
>();
|
|
95
94
|
|
|
95
|
+
/* oxlint-disable evolu/no-unnecessary-global-this -- Patch the global object constructors even if realm lexical bindings shadow them. */
|
|
96
96
|
const installAbortControllerPolyfills = (): void => {
|
|
97
97
|
installAbortReasonPolyfill(
|
|
98
98
|
globalThis.AbortController,
|
|
@@ -104,6 +104,7 @@ const installAbortControllerPolyfills = (): void => {
|
|
|
104
104
|
globalThis.AbortSignal,
|
|
105
105
|
);
|
|
106
106
|
};
|
|
107
|
+
/* oxlint-enable evolu/no-unnecessary-global-this */
|
|
107
108
|
|
|
108
109
|
const installAbortReasonPolyfill = (
|
|
109
110
|
abortController: AbortControllerConstructor,
|
|
@@ -176,6 +177,7 @@ const installAbortSignalStaticMethods = (
|
|
|
176
177
|
writable: true,
|
|
177
178
|
value: (milliseconds: number): AbortSignal => {
|
|
178
179
|
const controller = new abortController();
|
|
180
|
+
// oxlint-disable-next-line evolu/no-unnecessary-global-this -- Use the global object timer even if a realm lexical binding shadows it.
|
|
179
181
|
const timeoutId = globalThis.setTimeout(() => {
|
|
180
182
|
controller.abort(createTimeoutError(milliseconds));
|
|
181
183
|
}, milliseconds);
|
|
@@ -183,6 +185,7 @@ const installAbortSignalStaticMethods = (
|
|
|
183
185
|
controller.signal.addEventListener(
|
|
184
186
|
"abort",
|
|
185
187
|
() => {
|
|
188
|
+
// oxlint-disable-next-line evolu/no-unnecessary-global-this -- Clear the timer through the same global object API used to create it.
|
|
186
189
|
globalThis.clearTimeout(timeoutId);
|
|
187
190
|
},
|
|
188
191
|
{ once: true },
|
|
@@ -276,6 +279,7 @@ const createAnyControllersRegistry = (
|
|
|
276
279
|
|
|
277
280
|
return {
|
|
278
281
|
add: (controller) => {
|
|
282
|
+
// oxlint-disable-next-line evolu/no-unnecessary-global-this -- Use the constructor on the global object even if a realm lexical binding shadows it.
|
|
279
283
|
refs.push(new globalThis.WeakRef(controller));
|
|
280
284
|
cleanup();
|
|
281
285
|
},
|
|
@@ -305,6 +309,7 @@ const createAbortError = (): Error =>
|
|
|
305
309
|
const createNamedError = (name: string, message: string): Error => {
|
|
306
310
|
if (typeof globalThis.DOMException === "function") {
|
|
307
311
|
try {
|
|
312
|
+
// oxlint-disable-next-line evolu/no-unnecessary-global-this -- Construct the same global object DOMException verified by the feature check.
|
|
308
313
|
return new globalThis.DOMException(message, name);
|
|
309
314
|
} catch {
|
|
310
315
|
// Some runtimes expose DOMException but cannot construct it reliably.
|