@fable-org/fable-library-js 2.8.0 → 2.8.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/Async.js CHANGED
@@ -86,6 +86,9 @@ export function awaitEvent(event, cancelAction) {
86
86
  }
87
87
  ctx.onCancel(new OperationCanceledException());
88
88
  });
89
+ if (ctx.cancelToken.isCancelled) {
90
+ return;
91
+ }
89
92
  event.AddHandler(handler);
90
93
  });
91
94
  }
@@ -127,15 +130,26 @@ export function sequential(computations) {
127
130
  }
128
131
  export function sleep(millisecondsDueTime) {
129
132
  return protectedCont((ctx) => {
133
+ let completed = false;
130
134
  let tokenId;
131
- const timeoutId = setTimeout(() => {
132
- ctx.cancelToken.removeListener(tokenId);
133
- ctx.onSuccess(void 0);
134
- }, millisecondsDueTime);
135
- tokenId = ctx.cancelToken.addListener(() => {
135
+ const settle = (cancelled) => {
136
+ if (completed) {
137
+ return;
138
+ }
139
+ completed = true;
136
140
  clearTimeout(timeoutId);
137
- ctx.onCancel(new OperationCanceledException());
138
- });
141
+ if (tokenId !== undefined) {
142
+ ctx.cancelToken.removeListener(tokenId);
143
+ }
144
+ if (cancelled) {
145
+ ctx.onCancel(new OperationCanceledException());
146
+ }
147
+ else {
148
+ ctx.onSuccess(void 0);
149
+ }
150
+ };
151
+ const timeoutId = setTimeout(() => settle(false), millisecondsDueTime);
152
+ tokenId = ctx.cancelToken.addListener(() => settle(true));
139
153
  });
140
154
  }
141
155
  export function start(computation, cancellationToken) {
package/AsyncBuilder.js CHANGED
@@ -1,4 +1,12 @@
1
1
  import { Exception, ensureErrorOrException } from "./Util.js";
2
+ // AggregateError requires ES2021; the runtime also supports ES2020 hosts.
3
+ export class CancellationCallbackError extends Exception {
4
+ constructor(errors) {
5
+ // invariant: F# InnerException exposes the first failure without discarding the remaining errors
6
+ super("Cancellation callbacks failed", errors[0]);
7
+ this.errors = errors;
8
+ }
9
+ }
2
10
  export class CancellationToken {
3
11
  constructor(cancelled = false) {
4
12
  this._id = 0;
@@ -11,14 +19,35 @@ export class CancellationToken {
11
19
  cancel() {
12
20
  if (!this._cancelled) {
13
21
  this._cancelled = true;
14
- for (const [, listener] of this._listeners) {
15
- listener();
22
+ // decision: visit newest registrations first, matching .NET; disposal can skip pending callbacks
23
+ const pending = Array.from(this._listeners.keys()).reverse();
24
+ const errors = [];
25
+ for (const id of pending) {
26
+ const listener = this._listeners.get(id);
27
+ this._listeners.delete(id);
28
+ if (listener) {
29
+ try {
30
+ listener();
31
+ }
32
+ catch (error) {
33
+ errors.push(error);
34
+ }
35
+ }
36
+ }
37
+ if (errors.length > 0) {
38
+ throw new CancellationCallbackError(errors);
16
39
  }
17
40
  }
18
41
  }
19
42
  addListener(f) {
20
43
  const id = this._id;
21
- this._listeners.set(this._id++, f);
44
+ this._id++;
45
+ if (this._cancelled) {
46
+ f();
47
+ }
48
+ else {
49
+ this._listeners.set(id, f);
50
+ }
22
51
  return id;
23
52
  }
24
53
  removeListener(id) {
@@ -148,15 +177,31 @@ export class AsyncBuilder {
148
177
  return protectedCont((ctx) => {
149
178
  computation({
150
179
  onSuccess: (x) => {
151
- compensation();
180
+ try {
181
+ compensation();
182
+ }
183
+ catch (error) {
184
+ ctx.onError(ensureErrorOrException(error));
185
+ return;
186
+ }
152
187
  ctx.onSuccess(x);
153
188
  },
154
189
  onError: (x) => {
155
- compensation();
190
+ try {
191
+ compensation();
192
+ }
193
+ catch (error) {
194
+ ctx.onError(ensureErrorOrException(error));
195
+ return;
196
+ }
156
197
  ctx.onError(x);
157
198
  },
158
199
  onCancel: (x) => {
159
- compensation();
200
+ // decision: cancellation remains terminal when compensation throws, matching .NET Async.TryFinally
201
+ try {
202
+ compensation();
203
+ }
204
+ catch { /* Cancellation takes precedence. */ }
160
205
  ctx.onCancel(x);
161
206
  },
162
207
  cancelToken: ctx.cancelToken,
package/CHANGELOG.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36
2
+ last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8
3
3
  updaters:
4
4
  - package.json:
5
5
  file: package.json
@@ -15,6 +15,15 @@ All notable changes to this project will be documented in this file.
15
15
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
16
16
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
17
17
 
18
+ ## 2.8.1 - 2026-10-12
19
+
20
+ ### 🐞 Bug Fixes
21
+
22
+ * *(js/ts/python)* Synchronize cancellation registrations (#5036) ([07db576c](https://github.com/fable-compiler/Fable/commit/07db576c4ae16dcef9d1773ac4d6c70b2a223f0a))
23
+ * *(js/ts/python)* Settle cancelled delays and finalizers once (#5038) ([a9802484](https://github.com/fable-compiler/Fable/commit/a9802484c8cdeadd5203123d76f42e375c1c3159))
24
+
25
+ <strong><small>[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..148f989e1b37b3f6560c3dda615b6a3a8efffcb8)</small></strong>
26
+
18
27
  ## 2.8.0 - 2026-09-25
19
28
 
20
29
  ### 🐞 Bug Fixes
@@ -1,6 +1,7 @@
1
1
  import { defaultCancellationToken } from "./Async.js";
2
2
  import { fromContinuations } from "./Async.js";
3
3
  import { startImmediate } from "./Async.js";
4
+ import { OperationCanceledException, protectedCont } from "./AsyncBuilder.js";
4
5
  import { Exception } from "./Util.js";
5
6
  class QueueCell {
6
7
  constructor(message) {
@@ -55,12 +56,27 @@ export function startInstance($this) {
55
56
  startImmediate($this.body($this), $this.cancellationToken);
56
57
  }
57
58
  export function receive($this) {
58
- return fromContinuations((conts) => {
59
+ return protectedCont((ctx) => {
59
60
  if ($this.continuation) {
60
61
  throw new Exception("Receive can only be called once!");
61
62
  }
62
- $this.continuation = conts[0];
63
- __processEvents($this);
63
+ let tokenId;
64
+ const continuation = (message) => {
65
+ if (tokenId !== undefined) {
66
+ ctx.cancelToken.removeListener(tokenId);
67
+ }
68
+ ctx.onSuccess(message);
69
+ };
70
+ $this.continuation = continuation;
71
+ tokenId = ctx.cancelToken.addListener(() => {
72
+ if ($this.continuation === continuation) {
73
+ delete $this.continuation;
74
+ ctx.onCancel(new OperationCanceledException());
75
+ }
76
+ });
77
+ if ($this.continuation === continuation) {
78
+ __processEvents($this);
79
+ }
64
80
  });
65
81
  }
66
82
  export function post($this, message) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "name": "@fable-org/fable-library-js",
6
- "version": "2.8.0",
6
+ "version": "2.8.1",
7
7
  "description": "Core library used by F# projects compiled with fable.io",
8
8
  "author": "Fable Contributors",
9
9
  "license": "MIT",