Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
According to [The Big O of Code Reviews](https://www.egorand.dev/the-big-o-of-code-reviews/), this is a O(_n_) change. Fixes #73 Fixes #74 ### Background Task cancellation is cooperative in both Rust and Javascript. i.e. they rely on cooperation of the task itself to perform cancellation. In Javascript, there is API for the canceller: the `AbortController`. The `AbortController` has an `AbortSignal` object which can be given to the task. For example: ```typescript function cancellableDelayPromise( delayMs: number, abortSignal: AbortSignal, ): Promise<void> { return new Promise((resolve, reject) => { const timer = setTimeout(resolve, delayMs); abortSignal.addEventListener("abort", () => { clearTimeout(timer); reject(abortSignal.reason); }); }); } ``` This might be used like so: ```typescript const abortController = new AbortController(); setTimeout(() => abortController.abort(), 1000); // Abort the task below after 1 second. try { await cancellableDelayPromise(24 * 60 * 60 * 1000, abortController.signal); throw new Error("You're too late! It's 24 hours afterwards"); } catch (e: any) { assertTrue(e instanceof Error && e.name === "AbortError"); console.log("Phew, you didn't wait all that time"); } ``` ### Cancelling Rust tasks Uniffi's machinery provides a `cancelFunc`. As of `v0.28.0`, this causes the `Future` to be dropped. The Rust should be written in such a way as to do any cleanup for the task when this happens. This `cancelFunc` can be called indirectly by passing an `{ signal: AbortSignal; }` when calling any async function. There is no way for uniffi to know which Futures can be cancelled, so all async functions have an optional argument of `asyncOpts_?: { signal: AbortSignal; }` appended to their argument list. Thus: ```typscript await fetchUser(userId); ``` may also be called with an `AbortSignal`. ```typescript await fetchUser(userId, { signal }); ``` Since these are optional arguments, it is up to the Typescript caller whether or not to include them. ### Cancelling async Javascript callbacks The `futures_util` crate provides structures similar to `AbortController` and `AbortSignal`. In this example, `obj` is a JS callback interface. ```rust async fn cancel_delay_using_trait(obj: Arc<dyn AsyncParser>, delay_ms: i32) { let (abort_handle, abort_registration) = AbortHandle::new_pair(); thread::spawn(move || { // Simulate a different thread aborting the process thread::sleep(Duration::from_millis(1)); abort_handle.abort(); }); let future = Abortable::new(obj.delay(delay_ms), abort_registration); assert_eq!(future.await, Err(Aborted)); } ``` The `obj.delay(delay_ms)` call translates to a call to a Javascript function. ```typescript delay(delayMs: number, asyncOpts_?: { signal: AbortSignal })` ``` When `abort_handle.abort()` is called, the Abortable `future` is dropped. The `AbortSignal` in Javascript is told to abort when it is being cleaned up, and hasn't yet settled. Because uniffi can't tell which Javascript callbacks support an `AbortSignal`, all async functions have an optional argument of `asyncOpts_?: { signal: AbortSignal; }` appended to their argument list. Since these are optional arguments, it is up to the Typescript implementer whether or not to include them. ### Caveat emptor Because of the different APIs across languages _and_ the co-operative nature of task cancellation in Rust, there is a diversity of API support for task cancellation across the various backend languages that uniffi supports. This PR brings uniffi-bindgen-react-native to parity with the Mozilla supported languages. However, the uniffi docs currently suggest more modest support: ```md We don't directly support cancellation in UniFFI even when the underlying platforms do. You should build your cancellation in a separate, library specific channel; for example, exposing a `cancel()` method that sets a flag that the library checks periodically. Cancellation can then be exposed in the API and be mapped to one of the error variants, or None/empty-vec/whatever makes sense. There's no builtin way to cancel a future, nor to cause/raise a platform native async cancellation error (eg, a swift `CancellationError`). ``` I would expect this to change over time.
- Loading branch information