@fable-org/fable-library-js 2.8.0 → 2.9.0
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 +64 -9
- package/AsyncBuilder.js +70 -10
- package/CHANGELOG.md +23 -1
- package/Encoding.js +23 -16
- package/MailboxProcessor.js +23 -3
- package/package.json +1 -1
package/Async.js
CHANGED
|
@@ -2,7 +2,7 @@ import { OperationCanceledException, Trampoline } from "./AsyncBuilder.js";
|
|
|
2
2
|
import { CancellationToken } from "./AsyncBuilder.js";
|
|
3
3
|
import { protectedCont, protectedBind, protectedReturn } from "./AsyncBuilder.js";
|
|
4
4
|
import { Choice_makeChoice1Of2, Choice_makeChoice2Of2 } from "./Choice.js";
|
|
5
|
-
import { TimeoutException_$ctor } from "./System.js";
|
|
5
|
+
import { InvalidOperationException_$ctor_Z721C83C5, TimeoutException_$ctor } from "./System.js";
|
|
6
6
|
import { Exception } from "./Util.js";
|
|
7
7
|
function emptyContinuation(_x) {
|
|
8
8
|
// NOP
|
|
@@ -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
|
}
|
|
@@ -104,8 +107,49 @@ export function catchAsync(work) {
|
|
|
104
107
|
});
|
|
105
108
|
});
|
|
106
109
|
}
|
|
110
|
+
export function tryCancelled(computation, compensation) {
|
|
111
|
+
return (ctx) => {
|
|
112
|
+
// invariant: install the handler without adding a cancellation checkpoint, including for nested handlers
|
|
113
|
+
computation({
|
|
114
|
+
...ctx,
|
|
115
|
+
onCancel: (error) => {
|
|
116
|
+
// decision: preserve the original cancellation even when compensation throws, matching .NET
|
|
117
|
+
try {
|
|
118
|
+
compensation(error);
|
|
119
|
+
}
|
|
120
|
+
catch { /* Cancellation takes precedence. */ }
|
|
121
|
+
if (ctx.trampoline.incrementAndCheck()) {
|
|
122
|
+
ctx.trampoline.hijack(() => ctx.onCancel(error));
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
ctx.onCancel(error);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
}
|
|
107
131
|
export function fromContinuations(f) {
|
|
108
|
-
return
|
|
132
|
+
return (ctx) => {
|
|
133
|
+
let settled = false;
|
|
134
|
+
const claim = () => {
|
|
135
|
+
if (settled) {
|
|
136
|
+
throw InvalidOperationException_$ctor_Z721C83C5("A continuation was invoked more than once");
|
|
137
|
+
}
|
|
138
|
+
settled = true;
|
|
139
|
+
};
|
|
140
|
+
const success = (x) => { claim(); ctx.onSuccess(x); };
|
|
141
|
+
const error = (x) => { claim(); ctx.onError(x); };
|
|
142
|
+
const cancelled = (x) => { claim(); ctx.onCancel(x); };
|
|
143
|
+
protectedCont(() => f([success, error, cancelled]))({
|
|
144
|
+
onSuccess: success,
|
|
145
|
+
onError: (x) => { if (settled) {
|
|
146
|
+
throw x;
|
|
147
|
+
} error(x); },
|
|
148
|
+
onCancel: cancelled,
|
|
149
|
+
cancelToken: ctx.cancelToken,
|
|
150
|
+
trampoline: ctx.trampoline,
|
|
151
|
+
});
|
|
152
|
+
};
|
|
109
153
|
}
|
|
110
154
|
export function ignore(computation) {
|
|
111
155
|
return protectedBind(computation, (_x) => protectedReturn(void 0));
|
|
@@ -127,15 +171,26 @@ export function sequential(computations) {
|
|
|
127
171
|
}
|
|
128
172
|
export function sleep(millisecondsDueTime) {
|
|
129
173
|
return protectedCont((ctx) => {
|
|
174
|
+
let completed = false;
|
|
130
175
|
let tokenId;
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
176
|
+
const settle = (cancelled) => {
|
|
177
|
+
if (completed) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
completed = true;
|
|
136
181
|
clearTimeout(timeoutId);
|
|
137
|
-
|
|
138
|
-
|
|
182
|
+
if (tokenId !== undefined) {
|
|
183
|
+
ctx.cancelToken.removeListener(tokenId);
|
|
184
|
+
}
|
|
185
|
+
if (cancelled) {
|
|
186
|
+
ctx.onCancel(new OperationCanceledException());
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
ctx.onSuccess(void 0);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const timeoutId = setTimeout(() => settle(false), millisecondsDueTime);
|
|
193
|
+
tokenId = ctx.cancelToken.addListener(() => settle(true));
|
|
139
194
|
});
|
|
140
195
|
}
|
|
141
196
|
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
|
-
|
|
15
|
-
|
|
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.
|
|
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) {
|
|
@@ -145,24 +174,54 @@ export class AsyncBuilder {
|
|
|
145
174
|
return computation;
|
|
146
175
|
}
|
|
147
176
|
TryFinally(computation, compensation) {
|
|
148
|
-
|
|
149
|
-
|
|
177
|
+
// The cancellation check in protectedCont must see the cleanup continuations
|
|
178
|
+
return (ctx) => {
|
|
179
|
+
let settled = false;
|
|
180
|
+
protectedCont(computation)({
|
|
150
181
|
onSuccess: (x) => {
|
|
151
|
-
|
|
182
|
+
if (settled) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
settled = true;
|
|
186
|
+
try {
|
|
187
|
+
compensation();
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
ctx.onError(ensureErrorOrException(error));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
152
193
|
ctx.onSuccess(x);
|
|
153
194
|
},
|
|
154
195
|
onError: (x) => {
|
|
155
|
-
|
|
196
|
+
if (settled) {
|
|
197
|
+
throw x;
|
|
198
|
+
}
|
|
199
|
+
settled = true;
|
|
200
|
+
try {
|
|
201
|
+
compensation();
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
ctx.onError(ensureErrorOrException(error));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
156
207
|
ctx.onError(x);
|
|
157
208
|
},
|
|
158
209
|
onCancel: (x) => {
|
|
159
|
-
|
|
210
|
+
if (settled) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
settled = true;
|
|
214
|
+
// decision: cancellation remains terminal when compensation throws, matching .NET Async.TryFinally
|
|
215
|
+
try {
|
|
216
|
+
compensation();
|
|
217
|
+
}
|
|
218
|
+
catch { /* Cancellation takes precedence. */ }
|
|
160
219
|
ctx.onCancel(x);
|
|
161
220
|
},
|
|
162
221
|
cancelToken: ctx.cancelToken,
|
|
163
222
|
trampoline: ctx.trampoline,
|
|
164
223
|
});
|
|
165
|
-
}
|
|
224
|
+
};
|
|
166
225
|
}
|
|
167
226
|
TryWith(computation, catchHandler) {
|
|
168
227
|
return protectedCont((ctx) => {
|
|
@@ -187,7 +246,8 @@ export class AsyncBuilder {
|
|
|
187
246
|
});
|
|
188
247
|
}
|
|
189
248
|
Using(resource, binder) {
|
|
190
|
-
|
|
249
|
+
// Without Delay the binder runs before TryFinally installs Dispose
|
|
250
|
+
return this.TryFinally(this.Delay(() => binder(resource)), () => resource.Dispose());
|
|
191
251
|
}
|
|
192
252
|
While(guard, computation) {
|
|
193
253
|
if (guard()) {
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
last_commit_released:
|
|
2
|
+
last_commit_released: e33c57870454fea189a3eb33519f583a5003f8c3
|
|
3
3
|
updaters:
|
|
4
4
|
- package.json:
|
|
5
5
|
file: package.json
|
|
@@ -15,6 +15,28 @@ 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.9.0 - 2026-10-16
|
|
19
|
+
|
|
20
|
+
### 🚀 Features
|
|
21
|
+
|
|
22
|
+
* *(js/ts/python/beam)* Support Async.TryCancelled (#5083) ([1643297d](https://github.com/fable-compiler/Fable/commit/1643297d9297fef536c8d10091d5b0d1fad77af7))
|
|
23
|
+
|
|
24
|
+
### 🐞 Bug Fixes
|
|
25
|
+
|
|
26
|
+
* *(js/ts)* Avoid full array copy in Encoding.GetString (#5069) ([e6b72ec7](https://github.com/fable-compiler/Fable/commit/e6b72ec73be454bd3b3233167542a65fc6553c8a))
|
|
27
|
+
* *(js/ts/python)* Preserve cancelled cleanup and generic unit callbacks (#5062) ([5dd6b497](https://github.com/fable-compiler/Fable/commit/5dd6b4979e79c8f7c4632cbfb5254d6cf0d2e8d2))
|
|
28
|
+
|
|
29
|
+
<strong><small>[View changes on Github](https://github.com/fable-compiler/Fable/compare/148f989e1b37b3f6560c3dda615b6a3a8efffcb8..e33c57870454fea189a3eb33519f583a5003f8c3)</small></strong>
|
|
30
|
+
|
|
31
|
+
## 2.8.1 - 2026-10-12
|
|
32
|
+
|
|
33
|
+
### 🐞 Bug Fixes
|
|
34
|
+
|
|
35
|
+
* *(js/ts/python)* Synchronize cancellation registrations (#5036) ([07db576c](https://github.com/fable-compiler/Fable/commit/07db576c4ae16dcef9d1773ac4d6c70b2a223f0a))
|
|
36
|
+
* *(js/ts/python)* Settle cancelled delays and finalizers once (#5038) ([a9802484](https://github.com/fable-compiler/Fable/commit/a9802484c8cdeadd5203123d76f42e375c1c3159))
|
|
37
|
+
|
|
38
|
+
<strong><small>[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..148f989e1b37b3f6560c3dda615b6a3a8efffcb8)</small></strong>
|
|
39
|
+
|
|
18
40
|
## 2.8.0 - 2026-09-25
|
|
19
41
|
|
|
20
42
|
### 🐞 Bug Fixes
|
package/Encoding.js
CHANGED
|
@@ -90,6 +90,27 @@ function utf8_decode(bytes) {
|
|
|
90
90
|
}
|
|
91
91
|
return chars.join("");
|
|
92
92
|
}
|
|
93
|
+
function toUint8Array(bytes, index, count) {
|
|
94
|
+
if (ArrayBuffer.isView(bytes)) {
|
|
95
|
+
const buffer = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
96
|
+
if (index != null && count != null) {
|
|
97
|
+
return buffer.subarray(index, index + count);
|
|
98
|
+
}
|
|
99
|
+
else if (index != null) {
|
|
100
|
+
return buffer.subarray(index);
|
|
101
|
+
}
|
|
102
|
+
return buffer;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
if (index != null && count != null) {
|
|
106
|
+
return Uint8Array.from(Array.prototype.slice.call(bytes, index, index + count));
|
|
107
|
+
}
|
|
108
|
+
else if (index != null) {
|
|
109
|
+
return Uint8Array.from(Array.prototype.slice.call(bytes, index));
|
|
110
|
+
}
|
|
111
|
+
return Uint8Array.from(bytes);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
93
114
|
class UTF16LE {
|
|
94
115
|
getBytes(str, index, count) {
|
|
95
116
|
str = Array.isArray(str) ? str.join("") : str;
|
|
@@ -111,14 +132,7 @@ class UTF16LE {
|
|
|
111
132
|
}
|
|
112
133
|
}
|
|
113
134
|
getString(bytes, index, count) {
|
|
114
|
-
const
|
|
115
|
-
let buffer = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
|
|
116
|
-
if (index != null && count != null) {
|
|
117
|
-
buffer = buffer.subarray(index, index + count);
|
|
118
|
-
}
|
|
119
|
-
else if (index != null) {
|
|
120
|
-
buffer = buffer.subarray(index);
|
|
121
|
-
}
|
|
135
|
+
const buffer = toUint8Array(bytes, index, count);
|
|
122
136
|
// Allow to compile for Browser environment without @types/node being installed
|
|
123
137
|
// See https://github.com/fable-compiler/Fable/issues/4368
|
|
124
138
|
const g = globalThis;
|
|
@@ -157,14 +171,7 @@ class UTF8 {
|
|
|
157
171
|
}
|
|
158
172
|
}
|
|
159
173
|
getString(bytes, index, count) {
|
|
160
|
-
const
|
|
161
|
-
let buffer = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
|
|
162
|
-
if (index != null && count != null) {
|
|
163
|
-
buffer = buffer.subarray(index, index + count);
|
|
164
|
-
}
|
|
165
|
-
else if (index != null) {
|
|
166
|
-
buffer = buffer.subarray(index);
|
|
167
|
-
}
|
|
174
|
+
const buffer = toUint8Array(bytes, index, count);
|
|
168
175
|
// Allow to compile for Browser environment without @types/node being installed
|
|
169
176
|
// See https://github.com/fable-compiler/Fable/issues/4368
|
|
170
177
|
const g = globalThis;
|
package/MailboxProcessor.js
CHANGED
|
@@ -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
|
|
59
|
+
return protectedCont((ctx) => {
|
|
59
60
|
if ($this.continuation) {
|
|
60
61
|
throw new Exception("Receive can only be called once!");
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
|
|
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) {
|
|
@@ -80,6 +96,10 @@ export function postAndAsyncReply($this, buildMessage) {
|
|
|
80
96
|
}
|
|
81
97
|
const reply = {
|
|
82
98
|
reply: (res) => {
|
|
99
|
+
// .NET AsyncReplyChannel.Reply ignores a second reply
|
|
100
|
+
if (replied) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
83
103
|
result = res;
|
|
84
104
|
replied = true;
|
|
85
105
|
checkCompletion();
|
package/package.json
CHANGED