Skip to content
Open
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
47 changes: 44 additions & 3 deletions src/Fable.Transforms/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,8 @@ let tryEntityIdent (com: Compiler) entFullName =
| Types.matchFail -> makeImportLib com Any "MatchFailureException" "Types" |> Some
| Types.exception_ -> makeImportLib com Any "Exception" "Util" |> Some
| "System.OperationCanceledException" -> makeImportLib com Any "OperationCanceledException" "AsyncBuilder" |> Some
| "System.Threading.Tasks.TaskCanceledException" ->
makeImportLib com Any "TaskCanceledException" "AsyncBuilder" |> Some
| "System.Collections.Generic.KeyNotFoundException" ->
makeImportLib com Any "KeyNotFoundException" "System.Collections.Generic"
|> Some
Expand Down Expand Up @@ -3292,6 +3294,19 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr
match i.DeclaringEntityFullName with
| "System.Collections.Generic.KeyNotFoundException"
| BuiltinSystemException _ -> bclType com ctx r t i thisArg args
// The library classes have no CancellationToken or Task property
| "System.OperationCanceledException"
| "System.Threading.Tasks.TaskCanceledException" as entName ->
let args =
match List.tryLast i.SignatureArgTypes with
| Some(DeclaredTypeFullName(Types.cancellationToken | Types.task)) ->
List.truncate (List.length args - 1) args
| _ -> args

let className = entName.Substring(entName.LastIndexOf('.') + 1)

Helper.LibCall(com, "AsyncBuilder", className, t, args, isConstructor = true, ?loc = r)
|> Some
| _ ->
Helper.LibCall(com, "Util", "Exception", Any, args, isConstructor = true)
|> Some
Expand Down Expand Up @@ -4038,12 +4053,15 @@ let random (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti
| _ -> None

let cancels (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) =
let isRegistration =
i.DeclaringEntityFullName = "System.Threading.CancellationTokenRegistration"

match i.CompiledName with
| "get_None" // TODO: implement as non-cancellable token
| ".ctor" ->
Helper.LibCall(com, "Async", "createCancellationToken", t, args, i.SignatureArgTypes)
|> Some
| "get_Token" -> thisArg
| "get_Token" when not isRegistration -> thisArg
| "Cancel"
| "CancelAfter"
| "get_IsCancellationRequested"
Expand All @@ -4063,8 +4081,21 @@ let cancels (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt
?loc = r
)
|> Some
| "CreateLinkedTokenSource" ->
let tokens =
match args with
| [ ExprType(Array _) as tokens ] -> tokens
| tokens -> makeArray (List.head i.SignatureArgTypes) tokens

Helper.LibCall(com, "Async", "createLinkedTokenSource", t, [ tokens ], ?loc = r)
|> Some
// A default registration is null, see disposables
| "Dispose" when isRegistration ->
Helper.LibCall(com, "Util", "disposeSafe", t, [ thisArg.Value ], ?loc = r)
|> Some
// TODO: Add check so CancellationTokenSource cannot be cancelled after disposed?
| "Dispose" -> Null Type.Unit |> makeValue r |> Some
| "Dispose"
| "Unregister" -> Helper.InstanceCall(thisArg.Value, i.CompiledName, t, [], ?loc = r) |> Some
| "Register" ->
Helper.InstanceCall(thisArg.Value, "register", t, args, i.SignatureArgTypes, ?loc = r)
|> Some
Expand Down Expand Up @@ -4354,7 +4385,7 @@ let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr lis
| "RunSynchronously" -> None
// Task<T> is Promise<T> in JS/TS
| "AwaitTask" ->
Helper.LibCall(com, "Async", "awaitPromise", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r)
Helper.LibCall(com, "Async", "awaitTask", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r)
|> Some
// Fable.Core extensions
| meth ->
Expand Down Expand Up @@ -4447,6 +4478,14 @@ let tasks (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio
?loc = r
)
|> Some
// Like awaiting on .NET, awaiters see the first exception of the seq overloads
| Some x, ("SetException" | "TrySetException" as meth) when
(match i.SignatureArgTypes with
| [ IEnumerable ] -> true
| _ -> false)
->
Helper.InstanceCall(x, meth + "s", t, args, i.SignatureArgTypes, ?loc = r)
|> Some
| Some x, meth ->
Helper.InstanceCall(x, meth, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r)
|> Some
Expand Down Expand Up @@ -4976,6 +5015,7 @@ let private replacedModules =
Types.random, random
"System.Threading.CancellationToken", cancels
"System.Threading.CancellationTokenSource", cancels
"System.Threading.CancellationTokenRegistration", cancels
"System.Threading.Monitor", monitor
"System.Activator", activator
"System.Text.Encoding", encoding
Expand All @@ -4999,6 +5039,7 @@ let private replacedModules =
"Microsoft.FSharp.Control.AsyncPrimitives", asyncs
Types.task, tasks
Types.taskGeneric, tasks
"System.Threading.Tasks.TaskCompletionSource", tasks
"System.Threading.Tasks.TaskCompletionSource`1", tasks
"System.Runtime.CompilerServices.TaskAwaiter`1", tasks
"Microsoft.FSharp.Control.TaskBuilder", taskBuilder
Expand Down
49 changes: 40 additions & 9 deletions src/fable-library-ts/Async.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { OperationCanceledException, Trampoline } from "./AsyncBuilder.ts";
import { OperationCanceledException, TaskCanceledException, Trampoline } from "./AsyncBuilder.ts";
import { Continuation, Continuations } from "./AsyncBuilder.ts";
import { Async, IAsyncContext, CancellationToken } from "./AsyncBuilder.ts";
import { protectedCont, protectedBind, protectedReturn } from "./AsyncBuilder.ts";
import type { IEvent$2 } from "./Event.ts";
import { FSharpChoice$2_$union, Choice_makeChoice1Of2, Choice_makeChoice2Of2 } from "./Choice.ts";
import { ArgumentException_$ctor_Z384F8060, InvalidOperationException_$ctor_Z721C83C5, TimeoutException_$ctor } from "./System.ts";
import { ArgumentException_$ctor_Z384F8060, ArgumentException_$ctor_Z721C83C5, InvalidOperationException_$ctor_Z721C83C5, TimeoutException_$ctor } from "./System.ts";
import { Exception, ensureErrorOrException, IDisposable } from "./Util.ts";
import type { Option } from "./Option.ts";
import { markObserved } from "./Task.ts";

function emptyContinuation<T>(_x: T) {
// NOP
Expand Down Expand Up @@ -51,15 +52,35 @@ export function isCancellationRequested(token: CancellationToken) {

export function throwIfCancellationRequested(token: CancellationToken) {
if (token != null && token.isCancelled) {
throw new Exception("Operation is cancelled");
throw new OperationCanceledException();
}
}

// Like .NET, the parents keep their listener until the linked token is disposed
class LinkedCancellationToken extends CancellationToken {
private readonly _unlink: () => void;
constructor(tokens: CancellationToken[]) {
super();
// A default token is null and never cancelled
const parents = tokens.filter((parent) => parent != null);
const ids = parents.map((parent) => parent.addListener(() => this.cancel()));
this._unlink = () => { parents.forEach((parent, i) => parent.removeListener(ids[i])); };
}
public Dispose() {
this._unlink();
}
}

// The parent keeps the listener until unlink is called
function createLinkedToken(parent: CancellationToken): [token: CancellationToken, unlink: () => void] {
const token = new CancellationToken();
const id = parent.addListener(() => token.cancel());
return [token, () => { parent.removeListener(id); }];
const token = new LinkedCancellationToken([parent]);
return [token, () => { token.Dispose(); }];
}

export function createLinkedTokenSource(tokens: CancellationToken[]): CancellationToken {
if (tokens.length === 0) {
throw ArgumentException_$ctor_Z721C83C5("No tokens were supplied.");
}
return new LinkedCancellationToken(tokens);
}

export function startChild<T>(computation: Async<T>, ms?: number): Async<Async<T>> {
Expand Down Expand Up @@ -104,6 +125,13 @@ export function awaitPromise<T>(p: Promise<T>) {
? conts[2] : conts[1])(err)));
}

// Like .NET, a cancelled task raises TaskCanceledException in the awaiting workflow instead of cancelling it
export function awaitTask<T>(task: Promise<T>) {
return fromContinuations((conts: Continuations<T>) =>
task.then(conts[0], (err) =>
conts[1](err instanceof OperationCanceledException ? new TaskCanceledException() : err)));
}

export function awaitEvent<Del extends Function, T>(event: IEvent$2<Del, T>, cancelAction?: () => void): Async<T> {
return protectedCont((ctx: IAsyncContext<T>) => {
let tokenId: number;
Expand Down Expand Up @@ -370,9 +398,12 @@ export function startAsPromise<T>(computation: Async<T>, cancellationToken?: Can
}

export function startAsTask<T>(computation: Async<T>, _taskCreationOptions?: number, cancellationToken?: CancellationToken) {
return startAsPromise(computation, cancellationToken);
return startImmediateAsTask(computation, cancellationToken);
}

// Like .NET, cancelling the computation cancels the task, and awaiting that raises TaskCanceledException
export function startImmediateAsTask<T>(computation: Async<T>, cancellationToken?: CancellationToken) {
return startAsPromise(computation, cancellationToken);
return markObserved(new Promise((resolve: Continuation<T>, reject: Continuation<any>) =>
startWithContinuations(computation, resolve, reject, () => reject(new TaskCanceledException()),
cancellationToken ? cancellationToken : defaultCancellationToken)));
}
15 changes: 12 additions & 3 deletions src/fable-library-ts/AsyncBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ export class CancellationToken implements IDisposable {
public register(f: (state?: any) => void, state?: any) {
const $ = this;
const id = this.addListener(state == null ? f : () => f(state));
return { Dispose() { $.removeListener(id); } };
return {
Dispose() { $.removeListener(id); },
Unregister() { return $.removeListener(id); },
};
}
public Dispose() {
// Implement IDisposable for compatibility but do nothing
Expand All @@ -72,12 +75,18 @@ export class CancellationToken implements IDisposable {
}

export class OperationCanceledException extends Exception {
constructor(msg?: string) {
super(msg ?? "The operation was canceled");
constructor(msg?: string, innerException?: Exception) {
super(msg ?? "The operation was canceled.", innerException);
// Object.setPrototypeOf(this, OperationCanceledException.prototype);
}
}

export class TaskCanceledException extends OperationCanceledException {
constructor(msg?: string, innerException?: Exception) {
super(msg ?? "A task was canceled.", innerException);
}
}

export class Trampoline {
static get maxTrampolineCallCount() {
return 2000;
Expand Down
68 changes: 58 additions & 10 deletions src/fable-library-ts/Task.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,84 @@
import { CancellationToken, OperationCanceledException } from "./AsyncBuilder.ts";
import { CancellationToken, TaskCanceledException } from "./AsyncBuilder.ts";
import { ArgumentException_$ctor_Z384F8060, InvalidOperationException_$ctor_Z721C83C5 } from "./System.ts";

export class TaskCompletionSource<T> {
private _resolve!: (value: T) => void;
private _reject!: (reason?: unknown) => void;
private _completed = false;
public task: Promise<T>;

constructor() {
this.task = new Promise<T>((resolve, reject) => {
this.task = markObserved(new Promise<T>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
}));
}

SetResult(value: T): void { this._resolve(value); }
SetException(error: unknown): void { this._reject(error); }
SetCancelled(): void { this._reject(new OperationCanceledException()); }
private trySettle(settle: () => void): boolean {
if (this._completed) { return false; }
this._completed = true;
settle();
return true;
}

private settle(settle: () => void): void {
if (!this.trySettle(settle)) {
throw InvalidOperationException_$ctor_Z721C83C5("An attempt was made to transition a task to a final state when it had already completed.");
}
}

SetResult(value: T): void { this.settle(() => this._resolve(value)); }
TrySetResult(value: T): boolean { return this.trySettle(() => this._resolve(value)); }
SetException(error: unknown): void { this.settle(() => this._reject(error)); }
TrySetException(error: unknown): boolean { return this.trySettle(() => this._reject(error)); }
SetExceptions(errors: Iterable<unknown>): void { this.SetException(firstException(errors)); }
TrySetExceptions(errors: Iterable<unknown>): boolean { return this.TrySetException(firstException(errors)); }
SetCanceled(): void { this.settle(() => this._reject(new TaskCanceledException())); }
TrySetCanceled(): boolean { return this.trySettle(() => this._reject(new TaskCanceledException())); }
// Former spelling of SetCanceled
SetCancelled(): void { this.SetCanceled(); }
get_Task(): Promise<T> { return this.task; }
}

// Awaiting a task faulted with several exceptions throws the first one on .NET
function firstException(errors: Iterable<unknown>): unknown {
const all = Array.from(errors);
if (all.length === 0) {
throw ArgumentException_$ctor_Z384F8060("The exceptions collection was empty.", "exceptions");
}
if (all.some((error) => error == null)) {
throw ArgumentException_$ctor_Z384F8060("The exceptions collection included at least one null element.", "exceptions");
}
return all[0];
}

export function fromResult<T>(value: T): Promise<T> {
return Promise.resolve(value);
}

// Like .NET, a failed task that nothing awaits is not an error, while awaiters still see the failure
export function markObserved<T>(task: Promise<T>): Promise<T> {
task.then(undefined, () => { /* NOP */ });
return task;
}

// Like .NET, the function runs after the caller continues and not at all if the token is cancelled by then
export function run<T>(f: () => T | PromiseLike<T>, cancellationToken?: CancellationToken): Promise<T> {
return markObserved(Promise.resolve().then(() => {
if (cancellationToken?.isCancelled) { throw new TaskCanceledException(); }
return f();
}));
}

export function zero(): Promise<void> {
return Promise.resolve();
}

// Like .NET, -1 waits until the token is cancelled
export function delay(millisecondsDelay: number, cancellationToken?: CancellationToken): Promise<void> {
return new Promise<void>((resolve, reject) => {
return markObserved(new Promise<void>((resolve, reject) => {
if (cancellationToken?.isCancelled) {
reject(new OperationCanceledException());
reject(new TaskCanceledException());
return;
}
let tokenId: number | undefined;
Expand All @@ -41,10 +89,10 @@ export function delay(millisecondsDelay: number, cancellationToken?: Cancellatio
if (cancellationToken != null) {
tokenId = cancellationToken.addListener(() => {
clearTimeout(timeoutId);
reject(new OperationCanceledException());
reject(new TaskCanceledException());
});
}
});
}));
}

// Task<T> = Promise<T> in JS/TS. GetAwaiter/GetResult return the Promise itself;
Expand Down
Loading
Loading