@fable-org/fable-library-js 2.4.1 → 2.4.2

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
@@ -7,10 +7,6 @@ import { Exception } from "./Util.js";
7
7
  function emptyContinuation(_x) {
8
8
  // NOP
9
9
  }
10
- // see AsyncBuilder.Delay
11
- function delay(generator) {
12
- return protectedCont((ctx) => generator()(ctx));
13
- }
14
10
  // MakeAsync: body:(AsyncActivation<'T> -> AsyncReturn) -> Async<'T>
15
11
  export function makeAsync(body) {
16
12
  return body;
@@ -49,18 +45,27 @@ export function throwIfCancellationRequested(token) {
49
45
  }
50
46
  }
51
47
  export function startChild(computation, ms) {
52
- const promise = startAsPromise(computation);
53
- let promiseToRun = promise;
54
- if (ms) {
55
- // Race the computation against a timeout: whichever settles first wins.
56
- promiseToRun = new Promise((resolve, reject) => {
57
- const timeoutId = setTimeout(() => reject(TimeoutException_$ctor()), ms);
58
- promise.then(value => { clearTimeout(timeoutId); resolve(value); }, error => { clearTimeout(timeoutId); reject(error); });
59
- });
60
- }
61
- // JS Promises are hot, computation has already started
62
- // but we delay returning the result
63
- return protectedCont((ctx) => protectedReturn(awaitPromise(promiseToRun))(ctx));
48
+ return protectedCont((ctx) => {
49
+ // Share the parent's cancellation token so cancelling the parent
50
+ // cancels the child, like .NET Async.StartChild
51
+ const promise = startAsPromise(computation, ctx.cancelToken);
52
+ let promiseToRun = promise;
53
+ if (ms) {
54
+ // Race the computation against a timeout: whichever settles first wins.
55
+ promiseToRun = new Promise((resolve, reject) => {
56
+ const timeoutId = setTimeout(() => reject(TimeoutException_$ctor()), ms);
57
+ promise.then(value => { clearTimeout(timeoutId); resolve(value); }, error => { clearTimeout(timeoutId); reject(error); });
58
+ });
59
+ }
60
+ // Prevent an unhandled rejection when the child is cancelled or fails
61
+ // while nobody awaits it (in F# an unobserved child's cancellation or
62
+ // failure is simply not propagated). awaitPromise attaches its own
63
+ // handlers to the same promise, so awaiting still observes the result.
64
+ promiseToRun.catch(emptyContinuation);
65
+ // JS Promises are hot, computation has already started
66
+ // but we delay returning the result
67
+ protectedReturn(awaitPromise(promiseToRun))(ctx);
68
+ });
64
69
  }
65
70
  export function awaitPromise(p) {
66
71
  return fromContinuations((conts) => p.then(conts[0]).catch((err) => (err instanceof OperationCanceledException
@@ -106,17 +111,19 @@ export function ignore(computation) {
106
111
  return protectedBind(computation, (_x) => protectedReturn(void 0));
107
112
  }
108
113
  export function parallel(computations) {
109
- return delay(() => awaitPromise(Promise.all(Array.from(computations, (w) => startAsPromise(w)))));
114
+ // Children share the parent's cancellation token, like .NET Async.Parallel
115
+ return protectedCont((ctx) => awaitPromise(Promise.all(Array.from(computations, (w) => startAsPromise(w, ctx.cancelToken))))(ctx));
110
116
  }
111
117
  export function sequential(computations) {
112
- function _sequential(computations) {
118
+ function _sequential(computations, cancelToken) {
113
119
  let pr = Promise.resolve([]);
114
120
  for (const c of computations) {
115
- pr = pr.then(results => startAsPromise(c).then(r => results.concat([r])));
121
+ pr = pr.then(results => startAsPromise(c, cancelToken).then(r => results.concat([r])));
116
122
  }
117
123
  return pr;
118
124
  }
119
- return delay(() => awaitPromise(_sequential(computations)));
125
+ // Children share the parent's cancellation token, like .NET Async.Sequential
126
+ return protectedCont((ctx) => awaitPromise(_sequential(computations, ctx.cancelToken))(ctx));
120
127
  }
121
128
  export function sleep(millisecondsDueTime) {
122
129
  return protectedCont((ctx) => {
@@ -139,10 +146,14 @@ export function startImmediate(computation, cancellationToken) {
139
146
  }
140
147
  export function startWithContinuations(computation, continuation, exceptionContinuation, cancellationContinuation, cancelToken) {
141
148
  const trampoline = new Trampoline();
149
+ // Mark the computation completed as soon as a terminal continuation is entered
150
+ // so protectedCont lets exceptions from continuation code propagate instead of
151
+ // routing them to onError (see Trampoline.completed).
152
+ const done = (cont) => (x) => { trampoline.completed = true; return cont(x); };
142
153
  computation({
143
- onSuccess: continuation ? continuation : emptyContinuation,
144
- onError: exceptionContinuation,
145
- onCancel: cancellationContinuation,
154
+ onSuccess: done(continuation ? continuation : emptyContinuation),
155
+ onError: done(exceptionContinuation),
156
+ onCancel: done(cancellationContinuation),
146
157
  cancelToken: cancelToken ? cancelToken : defaultCancellationToken,
147
158
  trampoline,
148
159
  });
package/AsyncBuilder.js CHANGED
@@ -47,6 +47,7 @@ export class Trampoline {
47
47
  }
48
48
  constructor() {
49
49
  this.callCount = 0;
50
+ this.completed = false;
50
51
  }
51
52
  incrementAndCheck() {
52
53
  return this.callCount++ > Trampoline.maxTrampolineCallCount;
@@ -67,6 +68,9 @@ export function protectedCont(f) {
67
68
  f(ctx);
68
69
  }
69
70
  catch (err) {
71
+ if (ctx.trampoline.completed) {
72
+ throw err;
73
+ }
70
74
  ctx.onError(ensureErrorOrException(err));
71
75
  }
72
76
  });
@@ -76,6 +80,13 @@ export function protectedCont(f) {
76
80
  f(ctx);
77
81
  }
78
82
  catch (err) {
83
+ // Once a terminal continuation has run the computation is complete, so
84
+ // an exception from user continuation code must propagate rather than be
85
+ // routed to onError (which would resolve the computation a second time,
86
+ // e.g. a succeeded try/with body re-entering its with handler).
87
+ if (ctx.trampoline.completed) {
88
+ throw err;
89
+ }
79
90
  ctx.onError(ensureErrorOrException(err));
80
91
  }
81
92
  }
@@ -85,12 +96,18 @@ export function protectedBind(computation, binder) {
85
96
  return protectedCont((ctx) => {
86
97
  computation({
87
98
  onSuccess: (x) => {
99
+ // Only guard the binder evaluation itself: the resulting computation
100
+ // is protected on its own, and re-catching what it throws would
101
+ // route continuation exceptions back into onError (double-resolve).
102
+ let bound;
88
103
  try {
89
- binder(x)(ctx);
104
+ bound = binder(x);
90
105
  }
91
106
  catch (err) {
92
107
  ctx.onError(ensureErrorOrException(err));
108
+ return;
93
109
  }
110
+ bound(ctx);
94
111
  },
95
112
  onError: ctx.onError,
96
113
  onCancel: ctx.onCancel,
@@ -155,12 +172,16 @@ export class AsyncBuilder {
155
172
  cancelToken: ctx.cancelToken,
156
173
  trampoline: ctx.trampoline,
157
174
  onError: (ex) => {
175
+ // See protectedBind: only guard the handler evaluation itself.
176
+ let handled;
158
177
  try {
159
- catchHandler(ex)(ctx);
178
+ handled = catchHandler(ex);
160
179
  }
161
180
  catch (err) {
162
181
  ctx.onError(ensureErrorOrException(err));
182
+ return;
163
183
  }
184
+ handled(ctx);
164
185
  },
165
186
  });
166
187
  });
package/BitConverter.js CHANGED
@@ -77,7 +77,7 @@ export function doubleToInt64Bits(value) {
77
77
  export function toBoolean(bytes, offset) {
78
78
  const array = ArrayBuffer.isView(bytes) ? bytes : Uint8Array.from(bytes);
79
79
  const view = new DataView(array.buffer, array.byteOffset, array.byteLength);
80
- return view.getUint8(offset) === 1 ? true : false;
80
+ return view.getUint8(offset) !== 0;
81
81
  }
82
82
  export function toChar(bytes, offset) {
83
83
  const array = ArrayBuffer.isView(bytes) ? bytes : Uint8Array.from(bytes);
@@ -134,5 +134,5 @@ export function toString(bytes, offset, count) {
134
134
  else if (offset != null) {
135
135
  buffer = buffer.subarray(offset);
136
136
  }
137
- return Array.from(buffer).map((b) => ("0" + b.toString(16)).slice(-2)).join("-");
137
+ return Array.from(buffer).map((b) => ("0" + b.toString(16)).slice(-2).toUpperCase()).join("-");
138
138
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- last_commit_released: d6ae6bd3790b57b31941a118cdffaeb6a59155c3
2
+ last_commit_released: a8d9bf5f0b7cc4560bb107963348e765695c22b6
3
3
  updaters:
4
4
  - package.json:
5
5
  file: package.json
@@ -15,6 +15,16 @@ 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.4.2 - 2026-07-11
19
+
20
+ ### 🐞 Bug Fixes
21
+
22
+ * *(js/ts)* Async cancellation propagation and continuation double-resolve (#4746) ([c78a9a8e](https://github.com/fable-compiler/Fable/commit/c78a9a8ed20234feddf3375b61e91098f9261bee))
23
+ * *(js/ts)* Date arithmetic: PM parsing, AddMonths day clamp, AddYears offset (#4744) ([be0558eb](https://github.com/fable-compiler/Fable/commit/be0558ebe07f78f7a93ec0c34592d7c066b59fc7))
24
+ * *(js/ts)* Int64 non-decimal parsing and BitConverter .NET mismatches (#4748) ([2704d9c5](https://github.com/fable-compiler/Fable/commit/2704d9c5daab87eb1791c0ea1b6b8b06b987247c))
25
+
26
+ <strong><small>[View changes on Github](https://github.com/fable-compiler/Fable/compare/d6ae6bd3790b57b31941a118cdffaeb6a59155c3..a8d9bf5f0b7cc4560bb107963348e765695c22b6)</small></strong>
27
+
18
28
  ## 2.4.1 - 2026-07-09
19
29
 
20
30
  ### 🐞 Bug Fixes
package/Date.js CHANGED
@@ -595,7 +595,10 @@ export function parseRaw(input) {
595
595
  parseInt(timeParts[1] || "0", 10) * 60 +
596
596
  parseFloat(timeParts[2] || "0");
597
597
  if (m[3] != null && m[3].toUpperCase() === "PM" && hourPart < 12) {
598
- timeInSeconds += 720;
598
+ timeInSeconds += 12 * 3600;
599
+ }
600
+ else if (m[3] != null && m[3].toUpperCase() === "AM" && hourPart === 12) {
601
+ timeInSeconds -= 12 * 3600;
599
602
  }
600
603
  }
601
604
  if (m[4] != null) { // There's an offset, parse as UTC
@@ -784,19 +787,9 @@ export function addYears(d, v) {
784
787
  return create(newYear, newMonth, newDay, hour(d), minute(d), second(d), millisecond(d), d.kind);
785
788
  }
786
789
  export function addMonths(d, v) {
787
- let newMonth = month(d) + v;
788
- let newMonth_ = 0;
789
- let yearOffset = 0;
790
- if (newMonth > 12) {
791
- newMonth_ = newMonth % 12;
792
- yearOffset = Math.floor(newMonth / 12);
793
- newMonth = newMonth_;
794
- }
795
- else if (newMonth < 1) {
796
- newMonth_ = 12 + newMonth % 12;
797
- yearOffset = Math.floor(newMonth / 12) + (newMonth_ === 12 ? -1 : 0);
798
- newMonth = newMonth_;
799
- }
790
+ const totalMonths = month(d) + v;
791
+ const newMonth = ((totalMonths - 1) % 12 + 12) % 12 + 1;
792
+ const yearOffset = Math.floor((totalMonths - 1) / 12);
800
793
  const newYear = year(d) + yearOffset;
801
794
  const _daysInMonth = daysInMonth(newYear, newMonth);
802
795
  const newDay = Math.min(_daysInMonth, day(d));
package/DateOffset.js CHANGED
@@ -203,27 +203,18 @@ export function addTicks(d, v) {
203
203
  return add(d, toFloat64(v / 10000n));
204
204
  }
205
205
  export function addYears(d, v) {
206
- const newMonth = d.getUTCMonth() + 1;
207
- const newYear = d.getUTCFullYear() + v;
206
+ const d2 = new Date(d.getTime() + offset(d));
207
+ const newMonth = d2.getUTCMonth() + 1;
208
+ const newYear = d2.getUTCFullYear() + v;
208
209
  const _daysInMonth = daysInMonth(newYear, newMonth);
209
- const newDay = Math.min(_daysInMonth, d.getUTCDate());
210
- return create(newYear, newMonth, newDay, d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds(), offset(d));
210
+ const newDay = Math.min(_daysInMonth, d2.getUTCDate());
211
+ return create(newYear, newMonth, newDay, d2.getUTCHours(), d2.getUTCMinutes(), d2.getUTCSeconds(), d2.getUTCMilliseconds(), offset(d));
211
212
  }
212
213
  export function addMonths(d, v) {
213
214
  const d2 = new Date(d.getTime() + offset(d));
214
- let newMonth = d2.getUTCMonth() + 1 + v;
215
- let newMonth_ = 0;
216
- let yearOffset = 0;
217
- if (newMonth > 12) {
218
- newMonth_ = newMonth % 12;
219
- yearOffset = Math.floor(newMonth / 12);
220
- newMonth = newMonth_;
221
- }
222
- else if (newMonth < 1) {
223
- newMonth_ = 12 + newMonth % 12;
224
- yearOffset = Math.floor(newMonth / 12) + (newMonth_ === 12 ? -1 : 0);
225
- newMonth = newMonth_;
226
- }
215
+ const totalMonths = d2.getUTCMonth() + 1 + v;
216
+ const newMonth = ((totalMonths - 1) % 12 + 12) % 12 + 1;
217
+ const yearOffset = Math.floor((totalMonths - 1) / 12);
227
218
  const newYear = d2.getUTCFullYear() + yearOffset;
228
219
  const _daysInMonth = daysInMonth(newYear, newMonth);
229
220
  const newDay = Math.min(_daysInMonth, d2.getUTCDate());
package/Long.js CHANGED
@@ -1,37 +1,28 @@
1
1
  import { getPrefix, isValid } from "./Int32.js";
2
2
  import { fromString } from "./BigInt.js";
3
3
  import { Exception } from "./Util.js";
4
- function getMaxValue(unsigned, radix, isNegative) {
5
- switch (radix) {
6
- case 2: return unsigned ?
7
- "1111111111111111111111111111111111111111111111111111111111111111" :
8
- (isNegative ? "1000000000000000000000000000000000000000000000000000000000000000"
9
- : "111111111111111111111111111111111111111111111111111111111111111");
10
- case 8: return unsigned ?
11
- "1777777777777777777777" :
12
- (isNegative ? "1000000000000000000000" : "777777777777777777777");
13
- case 10: return unsigned ?
14
- "18446744073709551615" :
15
- (isNegative ? "9223372036854775808" : "9223372036854775807");
16
- case 16: return unsigned ?
17
- "FFFFFFFFFFFFFFFF" :
18
- (isNegative ? "8000000000000000" : "7FFFFFFFFFFFFFFF");
19
- default: throw new Exception("Invalid radix.");
4
+ function getRange(unsigned, bitsize) {
5
+ switch (bitsize) {
6
+ case 64: return unsigned ?
7
+ [0n, 18446744073709551615n] :
8
+ [-9223372036854775808n, 9223372036854775807n];
9
+ default: throw new Exception("Invalid bit size.");
20
10
  }
21
11
  }
22
- export function parse(str, style, unsigned, _bitsize, radix) {
12
+ export function parse(str, style, unsigned, bitsize, radix) {
23
13
  const res = isValid(str, style, radix);
24
14
  if (res != null) {
25
- const lessOrEqual = (x, y) => {
26
- const len = Math.max(x.length, y.length);
27
- return x.padStart(len, "0") <= y.padStart(len, "0");
28
- };
29
- const isNegative = res.sign === "-";
30
- const maxValue = getMaxValue(unsigned || res.radix !== 10, res.radix, isNegative);
31
- if (lessOrEqual(res.digits.toUpperCase(), maxValue)) {
32
- str = getPrefix(res.radix) + res.digits;
33
- str = isNegative ? res.sign + str : str;
34
- return fromString(str);
15
+ let v = fromString(getPrefix(res.radix) + res.digits);
16
+ if (res.sign === "-") {
17
+ v = -v;
18
+ }
19
+ const [umin, umax] = getRange(true, bitsize);
20
+ if (!unsigned && res.radix !== 10 && v >= umin && v <= umax) {
21
+ v = BigInt.asIntN(bitsize, v);
22
+ }
23
+ const [min, max] = getRange(unsigned, bitsize);
24
+ if (v >= min && v <= max) {
25
+ return v;
35
26
  }
36
27
  }
37
28
  throw new Exception(`The input string ${str} was not in a correct format.`);
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.4.1",
6
+ "version": "2.4.2",
7
7
  "description": "Core library used by F# projects compiled with fable.io",
8
8
  "author": "Fable Contributors",
9
9
  "license": "MIT",