flexdesk 0.17.4 → 0.17.6

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.
@@ -107,7 +107,8 @@ class DataSeriesPlotWindow {
107
107
  this._config = { ...saved };
108
108
  this._config.yColumns = (saved.yColumns || []).filter(c => this._numericColumns.has(c.header));
109
109
  } else {
110
- this._config = DataSeriesPlotWindow.createDefaultConfig(this.headers, this._numericColumns);
110
+ this._config = DataSeriesPlotWindow.createDefaultConfig(
111
+ this.headers, this._numericColumns, this.classifyHeader);
111
112
  }
112
113
 
113
114
  this.#createWindow();
@@ -694,13 +695,26 @@ class DataSeriesPlotWindow {
694
695
 
695
696
  // ── Default config factory ───────────────────────────────────────────
696
697
 
697
- static createDefaultConfig(headers, numericColumns) {
698
+ /**
699
+ * The config a series with no saved one opens with: the first numeric
700
+ * column `classifyHeader` calls `'time'` is X (else the first numeric
701
+ * column), and up to MAX_AUTO_COLUMNS other numeric columns are Y.
702
+ *
703
+ * Static, so it has no window: the header classifier is a parameter. A
704
+ * window passes its own (`options.classifyHeader`); a caller that passes
705
+ * none gets the default, which knows `t`/`time`/`step`/`period`.
706
+ *
707
+ * @param {string[]} headers
708
+ * @param {Set<string>} [numericColumns] - absent => every column is numeric
709
+ * @param {(header: string) => string} [classifyHeader=defaultClassifyHeader]
710
+ */
711
+ static createDefaultConfig(headers, numericColumns, classifyHeader = defaultClassifyHeader) {
698
712
  const isNumeric = numericColumns instanceof Set ? (h) => numericColumns.has(h) : () => true;
699
713
 
700
714
  // Find best X column (time-like, must be numeric)
701
715
  let xColumn = '';
702
716
  for (const h of headers) {
703
- if (isNumeric(h) && this.classifyHeader(h) === 'time') {
717
+ if (isNumeric(h) && classifyHeader(h) === 'time') {
704
718
  xColumn = h;
705
719
  break;
706
720
  }
@@ -22,7 +22,13 @@ export function setDefaultLanguage(id) {
22
22
  if (typeof id === 'string' && id) _defaultLanguage = id;
23
23
  }
24
24
 
25
- /** Dispose a Monaco editor, swallowing the benign "Canceled" error from pending async ops. */
25
+ /**
26
+ * Dispose a Monaco editor, swallowing a "Canceled" error that `dispose()` throws
27
+ * synchronously. It cannot catch the asynchronous kind — a pending Delayer's
28
+ * promise rejected by the dispose, which surfaces later as an unhandled
29
+ * rejection; the loader's window listener hides those (see
30
+ * `installCancellationGuard` in monaco_loader.js, BUG-0038).
31
+ */
26
32
  function safeDispose(editor) {
27
33
  try { editor.dispose(); } catch (e) { if (e?.message !== 'Canceled') throw e; }
28
34
  }
@@ -38,6 +38,38 @@ let _attempt = null;
38
38
  */
39
39
  export const monacoReady = new Promise(resolve => { _resolveMonaco = resolve; });
40
40
 
41
+ /**
42
+ * Monaco's asynchronous CancellationError is benign; keep it off the page.
43
+ *
44
+ * Monaco starts timers it never awaits: its WordHighlighter calls
45
+ * `runDelayer.trigger()` (a 50 ms Delayer) when an editor gets focus and
46
+ * drops the promise. Dispose the editor inside those 50 ms and
47
+ * `Delayer.dispose() -> cancel()` rejects that promise with a CancellationError
48
+ * (name and message both 'Canceled') that nothing handles, so the browser
49
+ * reports an uncaught 'Canceled' (BUG-0038). VS Code hides these with its own
50
+ * unhandledrejection handler; standalone Monaco has none, so FlexDesk installs
51
+ * one here: it calls preventDefault() for exactly that error and leaves every
52
+ * other rejection to surface as before.
53
+ *
54
+ * Installed once per page, by the first initMonaco() that starts a load, and
55
+ * kept for the life of the page — like Monaco itself, which cannot be unloaded.
56
+ */
57
+ let _cancellationGuardInstalled = false;
58
+
59
+ function isMonacoCancellation(reason) {
60
+ return reason?.name === 'Canceled' && reason?.message === 'Canceled';
61
+ }
62
+
63
+ function installCancellationGuard() {
64
+ if (_cancellationGuardInstalled) return;
65
+ _cancellationGuardInstalled = true;
66
+ // `?.`: the loader suites' stand-in pages (tests/monaco_env.mjs and the
67
+ // suites that set `window = globalThis` under Node) have no event target.
68
+ window.addEventListener?.('unhandledrejection', (event) => {
69
+ if (isMonacoCancellation(event.reason)) event.preventDefault();
70
+ });
71
+ }
72
+
41
73
  function injectLoaderScript(src) {
42
74
  return new Promise((resolve, reject) => {
43
75
  const s = document.createElement('script');
@@ -124,6 +156,10 @@ async function loadMonaco(basePath) {
124
156
  * given). An AMD-level failure is final for the page: every later call gets
125
157
  * the same rejection, and only a reload recovers (see `_attempt`).
126
158
  *
159
+ * The first call that starts a load also installs one window
160
+ * 'unhandledrejection' listener that hides Monaco's benign asynchronous
161
+ * CancellationError, and nothing else (see `installCancellationGuard`).
162
+ *
127
163
  * @param {string} [basePath] Path prefix for the vs/ directory. REQUIRED
128
164
  * unless an attempt is already in flight or has succeeded.
129
165
  * @returns {Promise<typeof monaco>}
@@ -136,6 +172,7 @@ export function initMonaco(basePath) {
136
172
  + "initMonaco('vendor/monaco'). Monaco is a peer dependency; the library "
137
173
  + 'does not guess where you put it.'));
138
174
  }
175
+ installCancellationGuard();
139
176
  const attempt = loadMonaco(basePath).then(
140
177
  (m) => { _resolveMonaco(m); return m; },
141
178
  (err) => {