@fable-org/fable-library-js 2.8.1 → 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 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
@@ -107,8 +107,49 @@ export function catchAsync(work) {
107
107
  });
108
108
  });
109
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
+ }
110
131
  export function fromContinuations(f) {
111
- return protectedCont((ctx) => f([ctx.onSuccess, ctx.onError, ctx.onCancel]));
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
+ };
112
153
  }
113
154
  export function ignore(computation) {
114
155
  return protectedBind(computation, (_x) => protectedReturn(void 0));
package/AsyncBuilder.js CHANGED
@@ -174,9 +174,15 @@ export class AsyncBuilder {
174
174
  return computation;
175
175
  }
176
176
  TryFinally(computation, compensation) {
177
- return protectedCont((ctx) => {
178
- computation({
177
+ // The cancellation check in protectedCont must see the cleanup continuations
178
+ return (ctx) => {
179
+ let settled = false;
180
+ protectedCont(computation)({
179
181
  onSuccess: (x) => {
182
+ if (settled) {
183
+ return;
184
+ }
185
+ settled = true;
180
186
  try {
181
187
  compensation();
182
188
  }
@@ -187,6 +193,10 @@ export class AsyncBuilder {
187
193
  ctx.onSuccess(x);
188
194
  },
189
195
  onError: (x) => {
196
+ if (settled) {
197
+ throw x;
198
+ }
199
+ settled = true;
190
200
  try {
191
201
  compensation();
192
202
  }
@@ -197,6 +207,10 @@ export class AsyncBuilder {
197
207
  ctx.onError(x);
198
208
  },
199
209
  onCancel: (x) => {
210
+ if (settled) {
211
+ return;
212
+ }
213
+ settled = true;
200
214
  // decision: cancellation remains terminal when compensation throws, matching .NET Async.TryFinally
201
215
  try {
202
216
  compensation();
@@ -207,7 +221,7 @@ export class AsyncBuilder {
207
221
  cancelToken: ctx.cancelToken,
208
222
  trampoline: ctx.trampoline,
209
223
  });
210
- });
224
+ };
211
225
  }
212
226
  TryWith(computation, catchHandler) {
213
227
  return protectedCont((ctx) => {
@@ -232,7 +246,8 @@ export class AsyncBuilder {
232
246
  });
233
247
  }
234
248
  Using(resource, binder) {
235
- return this.TryFinally(binder(resource), () => resource.Dispose());
249
+ // Without Delay the binder runs before TryFinally installs Dispose
250
+ return this.TryFinally(this.Delay(() => binder(resource)), () => resource.Dispose());
236
251
  }
237
252
  While(guard, computation) {
238
253
  if (guard()) {
package/CHANGELOG.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8
2
+ last_commit_released: e33c57870454fea189a3eb33519f583a5003f8c3
3
3
  updaters:
4
4
  - package.json:
5
5
  file: package.json
@@ -15,6 +15,19 @@ 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
+
18
31
  ## 2.8.1 - 2026-10-12
19
32
 
20
33
  ### 🐞 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 array = ArrayBuffer.isView(bytes) ? bytes : Uint8Array.from(bytes);
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 array = ArrayBuffer.isView(bytes) ? bytes : Uint8Array.from(bytes);
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;
@@ -96,6 +96,10 @@ export function postAndAsyncReply($this, buildMessage) {
96
96
  }
97
97
  const reply = {
98
98
  reply: (res) => {
99
+ // .NET AsyncReplyChannel.Reply ignores a second reply
100
+ if (replied) {
101
+ return;
102
+ }
99
103
  result = res;
100
104
  replied = true;
101
105
  checkCompletion();
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.1",
6
+ "version": "2.9.0",
7
7
  "description": "Core library used by F# projects compiled with fable.io",
8
8
  "author": "Fable Contributors",
9
9
  "license": "MIT",