-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update
ThreadAbortSignal
API to use classes. (#816)
- Loading branch information
Showing
12 changed files
with
291 additions
and
139 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
--- | ||
'@quilted/threads': major | ||
'@quilted/quilt': patch | ||
--- | ||
|
||
Changed `ThreadAbortSignal` utilities to be class-based instead of being a collection of utility functions. This change aligns the API more closely with `AbortController` in the browser, which is created with `new AbortController()`. | ||
|
||
Previously, you used `createThreadAbortSignal()` to serialize an `AbortSignal` to pass over a thread, and `acceptThreadAbortSignal()` to turn it into a “live” `AbortSignal`. With the new API, you will do the same steps, but with `ThreadAbortSignal.serialize()` and `new ThreadAbortSignal`: | ||
|
||
```ts | ||
import { | ||
createThreadAbortSignal, | ||
acceptThreadAbortSignal, | ||
} from '@quilted/threads'; | ||
|
||
const abortController = new AbortController(); | ||
const serializedAbortSignal = createThreadAbortSignal(abortController.signal); | ||
const liveAbortSignal = acceptThreadAbortSignal(serializedAbortSignal); | ||
|
||
await fetch('/', {signal: liveAbortSignal}); | ||
|
||
// Becomes: | ||
|
||
import { ThreadAbortSignal } from '@quilted/threads';\ | ||
|
||
const abortController = new AbortController(); | ||
const serializedAbortSignal = ThreadAbortSignal.serialize(abortController.signal); | ||
const liveAbortSignal = new ThreadAbortSignal(serializedAbortSignal); | ||
|
||
await fetch('/', {signal: liveAbortSignal}); | ||
``` | ||
|
||
Additionally, the new `ThreadAbortSignal` class assumes you are not doing manual memory management by default. If your target environment does not support automatic memory management of transferred functions, you will need to manually pass the `retain` and `release` functions to the new APIs: | ||
|
||
```ts | ||
import {retain, release, ThreadAbortSignal} from '@quilted/threads'; | ||
|
||
const abortController = new AbortController(); | ||
const serializedAbortSignal = ThreadAbortSignal.serialize( | ||
abortController.signal, | ||
{retain, release}, | ||
); | ||
const liveAbortSignal = new ThreadAbortSignal(serializedAbortSignal, { | ||
retain, | ||
release, | ||
}); | ||
|
||
await fetch('/', {signal: liveAbortSignal}); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
/** | ||
* A representation of an `AbortSignal` that can be serialized between | ||
* two threads. | ||
*/ | ||
export interface ThreadAbortSignalSerialization { | ||
/** | ||
* Whether the signal was already aborted at the time it was | ||
* sent to the sibling thread. | ||
*/ | ||
readonly aborted: boolean; | ||
|
||
/** | ||
* A function to connect the signal between the two threads. This | ||
* function should be called by the sibling thread when the abort | ||
* state changes (including changes since the thread-safe abort signal | ||
* was created). | ||
*/ | ||
start?(listener: (aborted: boolean) => void): void; | ||
} | ||
|
||
export interface ThreadAbortSignalOptions { | ||
/** | ||
* An optional function to call in order to manually retain the memory | ||
* associated with the `start` function of the serialized signal. | ||
* You only need to use this when using a strategy for serializing the | ||
* abort signal that requires manual memory management. | ||
*/ | ||
retain?(value: unknown): void; | ||
|
||
/** | ||
* An optional function to call in order to manually release the memory | ||
* associated with the `start` function of the serialized signal. | ||
* You only need to use this when using a strategy for serializing the | ||
* abort signal that requires manual memory management. | ||
*/ | ||
release?(value: unknown): void; | ||
} | ||
|
||
/** | ||
* Converts a serialized `AbortSignal` into a “live” one, which you can | ||
* use to cancel operations in the current environment. When the signal aborts, | ||
* all memory associated with the signal will be released automatically. | ||
*/ | ||
export class ThreadAbortSignal implements AbortSignal { | ||
#abortController: AbortController | undefined; | ||
#abortSignal: AbortSignal; | ||
#onabort: AbortSignal['onabort'] | null = null; | ||
|
||
// Proxy properties | ||
get aborted(): boolean { | ||
return this.#abortSignal.aborted; | ||
} | ||
|
||
get reason(): any { | ||
return this.#abortSignal.reason; | ||
} | ||
|
||
get onabort() { | ||
return this.#onabort; | ||
} | ||
|
||
set onabort(value) { | ||
if (this.#onabort) { | ||
this.#abortSignal.removeEventListener('abort', this.#onabort); | ||
} | ||
|
||
this.#onabort = value; | ||
|
||
if (value) { | ||
this.#abortSignal.addEventListener('abort', value); | ||
} | ||
} | ||
|
||
constructor( | ||
signal: AbortSignal | ThreadAbortSignalSerialization | undefined, | ||
{retain, release}: ThreadAbortSignalOptions = {}, | ||
) { | ||
if (isAbortSignal(signal)) { | ||
this.#abortSignal = signal; | ||
} else { | ||
this.#abortController = new AbortController(); | ||
this.#abortSignal = this.#abortController.signal; | ||
|
||
const {aborted, start} = signal ?? {}; | ||
|
||
if (aborted) { | ||
this.#abortController.abort(); | ||
} else if (start) { | ||
retain?.(start); | ||
|
||
start((aborted) => { | ||
if (aborted) this.#abortController!.abort(); | ||
}); | ||
|
||
if (release) { | ||
this.#abortSignal.addEventListener('abort', () => release(start), { | ||
once: true, | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Proxy methods | ||
addEventListener(...args: Parameters<AbortSignal['addEventListener']>) { | ||
return this.#abortSignal.addEventListener(...args); | ||
} | ||
|
||
removeEventListener(...args: Parameters<AbortSignal['removeEventListener']>) { | ||
return this.#abortSignal.removeEventListener(...args); | ||
} | ||
|
||
dispatchEvent(...args: Parameters<AbortSignal['dispatchEvent']>): boolean { | ||
return this.#abortSignal.dispatchEvent(...args); | ||
} | ||
|
||
throwIfAborted() { | ||
return this.#abortSignal.throwIfAborted(); | ||
} | ||
|
||
/** | ||
* Converts an `AbortSignal` into a version of that signal that can | ||
* be transferred to a target `Thread`. The resulting object can be | ||
* serialized using the RPC utilities provided in this library, and | ||
* passed to `new ThreadAbortSignal()` to be converted into a “live” | ||
* `AbortSignal`. | ||
*/ | ||
static serialize( | ||
signal: AbortSignal, | ||
{retain, release}: ThreadAbortSignalOptions = {}, | ||
): ThreadAbortSignalSerialization { | ||
if (signal.aborted) { | ||
return { | ||
aborted: true, | ||
}; | ||
} | ||
|
||
const listeners = new Set<(aborted: boolean) => void>(); | ||
|
||
signal.addEventListener( | ||
'abort', | ||
() => { | ||
for (const listener of listeners) { | ||
listener(signal.aborted); | ||
release?.(listener); | ||
} | ||
|
||
listeners.clear(); | ||
}, | ||
{once: true}, | ||
); | ||
|
||
return { | ||
aborted: false, | ||
start(listener) { | ||
if (signal.aborted) { | ||
listener(true); | ||
} else { | ||
retain?.(listener); | ||
listeners.add(listener); | ||
} | ||
}, | ||
}; | ||
} | ||
} | ||
|
||
function isAbortSignal(value: unknown): value is AbortSignal { | ||
return ( | ||
value != null && | ||
typeof (value as any).aborted === 'boolean' && | ||
typeof (value as any).addEventListener === 'function' | ||
); | ||
} |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.