arcane-os 0.16.3 → 0.18.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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.0
4
+
5
+ - Adopt published `node-http-server` 10.0.0. PEM-backed development and
6
+ packaged-preview HTTPS now negotiate HTTP/2 or HTTP/1.1 on the same port
7
+ through the existing module integration, with no new runtime dependency.
8
+ - Document the returned `Http2SecureServer` for PEM-backed HTTPS. Preserve
9
+ raw `tls` options on native `https.Server`, explicit HTTP development, the
10
+ HTTP mail gateway, paired HTTP `308` redirects, source/PWA routes, complete
11
+ responses, conditional caching, and owned listener shutdown.
12
+ - Adapt the shared synthetic TLS fixture to both native TLS constructors while
13
+ retaining option capture and cleanup. The fixture models routing and option
14
+ delegation; it does not establish a real TLS handshake or protocol negotiation.
15
+ - Report an occupied mail listener port clearly while retaining native
16
+ `EADDRINUSE` details, the original cause, and the existing startup cleanup.
17
+
18
+ ## 0.17.0
19
+
20
+ - Add neutral `modal.configure({dismissible})` configuration, defaulting to
21
+ `true`. Setting it to `false` hides the close button and prevents Escape,
22
+ backdrop and close-button dismissal while preserving the owner's existing
23
+ programmatic close/destroy lifecycle and running-task behavior.
24
+ - Retain the configuration through population, open/close cycles and task
25
+ completion. Keep focus within useful content when hiding a focused close
26
+ control, and preserve default modal behavior for existing consumers.
27
+
3
28
  ## 0.16.3
4
29
 
5
30
  - Remember browser-reported PWA installation in app-scoped DBOPFS and suppress
package/NOTICE CHANGED
@@ -8,7 +8,7 @@ informational notice and is not itself a grant of commercial rights.
8
8
  Third-party material retains its own terms:
9
9
 
10
10
  - event-pubsub 6.1.0: MIT License; see its installed `licence` file.
11
- - node-http-server 9.1.1: MIT License; see its installed `licence` file.
11
+ - node-http-server 10.0.0: MIT License; see its installed `licence` file.
12
12
  - strong-type 2.0.1: MIT License; see its installed `licence` file.
13
13
  - vanilla-test 2.1.3: MIT License; see its installed `licence` file.
14
14
  - ansi-colors-es6 5.0.0: MIT License; see its installed `LICENSE` file.
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.16.2` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.18.0` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -134,8 +134,10 @@ expiration. The timestamp advances only after the whole resource check succeeds.
134
134
  Restarting is not a content synchronization step; inspect the
135
135
  selected source route and the last successful check when evaluating freshness.
136
136
 
137
- The shared dev server uses RIAEvangelist's `node-http-server` public interface
138
- for HTTPS and conditional responses on those selected routes. The SDK
137
+ The shared dev server uses RIAEvangelist's published `node-http-server` 10.0.0
138
+ public interface for HTTPS and conditional responses on those selected routes.
139
+ PEM-backed HTTPS uses Node's `Http2SecureServer` with HTTP/1.1 compatibility;
140
+ clients negotiate either protocol on the same HTTPS port. The SDK
139
141
  owns source selection and generated representations. By default, source development
140
142
  and every packaged browser preview serve content on HTTPS and redirect their
141
143
  paired HTTP listener with status 308 through the public request hook.
@@ -345,6 +345,9 @@ Network URLs come from one interface snapshot at startup and do not establish
345
345
  remote reachability through the machine's firewall or network.
346
346
 
347
347
  HTTPS is the default for development and required for packaged browser previews.
348
+ The published `node-http-server` 10.0.0 integration negotiates HTTP/2 or HTTP/1.1
349
+ on the same HTTPS port using the configured PEM pair. Source routes, generated
350
+ PWA resources, and conditional responses use the same serving pipeline.
348
351
  `--https` remains accepted but is no longer needed to select the transport.
349
352
  `--port` selects the HTTPS application port. A second HTTP listener returns
350
353
  `308` redirects to that HTTPS port, preserving the requested path and query.
@@ -320,6 +320,13 @@ address and port. The server can serve callers from multiple domains on the
320
320
  same machine. Route the page's `/v1/mail` to this listener, or configure an
321
321
  explicit shared endpoint in the caller.
322
322
 
323
+ If the selected port is occupied, startup reports
324
+ `Mail port <port> is already taken, possibly by another mail server.`
325
+ The CLI exits with status 1. Programmatic callers receive the same message,
326
+ with native `EADDRINUSE` metadata and the original error retained as `cause`.
327
+ The existing listener remains running; this launch does not retry or select
328
+ another port.
329
+
323
330
  `--app` is an optional server event label and does not restrict incoming
324
331
  application names. `--from` is an optional shared sender override; omit it to
325
332
  preserve each report's sender or its provider template's default.
@@ -79,7 +79,7 @@ appropriate.
79
79
  | [`markdown-document.html`](#markdown-documenthtml) | Renders and navigates a complete Markdown document with focusable fragments. | `configure()`<br>`load()`<br>`render()`<br>`clear()`<br>`fail()`<br>`focus()`<br>`focusFragment()`<br>`destroy()` | `markdown-document-ready`<br>`markdown-document-state`<br>`markdown-document-loading`<br>`markdown-document-rendered`<br>`markdown-document-empty`<br>`markdown-document-error`<br>`markdown-document-navigate` | Complete Markdown/state normalized; malformed input and Marked/DOM failures remain visible |
80
80
  | [`markdown-editor.html`](#markdown-editorhtml) | Configurable Markdown authoring, toolbar, preview, title, and save surface. | `configure()`<br>`focus()`<br>`clear()`<br>`saveEntry()`<br>`destroy()` | `markdown-editor-ready`<br>`markdown-editor-change`<br>`markdown-editor-saved` | Editor values normalized; injected save result mixed |
81
81
  | [`media-embed.html`](#media-embedhtml) | Loads a parsed YouTube video or playlist embed with ordinary hosting by default, optional privacy enhancement, and an external-platform action. | `configure()`<br>`load()`<br>`destroy()` | `media-embed-ready`<br>`media-load`<br>`media-error`<br>`media-open-platform` | URL/error normalized; iframe/platform behavior native |
82
- | [`modal.html`](#modalhtml) | Generic modal with population, open/close, actions, and sequential task execution. | `populate()`<br>`open()`<br>`close()`<br>`runTasks()`<br>`destroy()` | `modal-ready`<br>`modal-opened`<br>`modal-closed`<br>`modal-action` | Modal state normalized; injected task results mixed |
82
+ | [`modal.html`](#modalhtml) | Generic modal with population, configurable user dismissal, actions, and concurrent task execution. | `configure()`<br>`populate()`<br>`open()`<br>`close()`<br>`runTasks()`<br>`destroy()`<br>`running`<br>`opened` | `modal-ready`<br>`modal-opened`<br>`modal-closed`<br>`modal-action` | Modal state normalized; injected task results mixed |
83
83
  | [`output-panel.html`](#output-panelhtml) | Presents status, output, body, coverage, actions, pending, error, and cleared states. | `configure()`<br>`setOutput()`<br>`setBody()`<br>`setCoverage()`<br>`setActions()`<br>`setPending()`<br>`setStatus()`<br>`setError()`<br>`clear()`<br>`destroy()` | `output-panel-ready`<br>`output-panel-state`<br>`output-panel-change`<br>`output-panel-action`<br>`output-panel-error`<br>`output-panel-cleared` | DOM-normalized |
84
84
  | [`preferences-form.html`](#preferences-formhtml) | Builds a schema-driven preferences form with submit, reset, busy, and status behavior. | `configure()`<br>`getValues()`<br>`setValues()`<br>`setBusy()`<br>`setStatus()`<br>`destroy()` | `preferences-form-ready`<br>`preferences-change`<br>`preferences-submit`<br>`preferences-reset` | Normalized form values |
85
85
  | [`pwa-install.html`](#pwa-installhtml) | Presents a dismissible browser installation action with floating or inline placement. | `configure()`<br>`install()`<br>`dismiss()`<br>`destroy()`<br>`state`<br>`ready` | `pwa-install-ready`<br>`pwa-install-change`<br>`pwa-install-dismissed` | Browser install availability and outcome supplied by the shared PWA owner |
@@ -944,11 +944,28 @@ Shared dependencies: [`YouTubeMedia.js`](runtime-modules.md#youtubemediajs).
944
944
 
945
945
  ### Overview
946
946
 
947
- Generic modal with population, open/close, actions, and sequential task execution.
947
+ Generic modal with population, configurable user dismissal, actions, and concurrent task execution.
948
948
 
949
949
  ### Public surface
950
950
 
951
- Methods/properties: `populate()`, `open()`, `close()`, `runTasks()`, `destroy()`.
951
+ Methods/properties: `configure()`, `populate()`, `open()`, `close()`, `runTasks()`,
952
+ `destroy()`, `running`, `opened`.
953
+
954
+ `configure({dismissible})` synchronously returns the current `{dismissible}`
955
+ configuration, or `false` after destruction. `dismissible` starts as `true`;
956
+ omitting it preserves the current choice. A supplied value must be a boolean.
957
+ Setting it to `false` hides the close button and prevents user dismissal through
958
+ Escape, backdrop clicks and the close button. Setting it back to `true` restores
959
+ normal user dismissal. The choice persists across population, open/close cycles
960
+ and task completion. If the close button held focus when disabled, focus moves
961
+ to a content control or the focusable modal body.
962
+
963
+ This option does not prevent the owning application from calling `close()` or
964
+ `destroy()`. The existing running-task behavior remains: `close()` leaves a
965
+ running modal open unless called with its existing second `force` argument set
966
+ to `true`; `destroy()` releases the component. Product-specific conditions for
967
+ closing a modal belong to the application. Content, actions and task results
968
+ are preserved, and `runTasks()` continues to execute independent jobs concurrently.
952
969
 
953
970
  Events: `modal-ready`, `modal-opened`, `modal-closed`, `modal-action`.
954
971
 
@@ -3464,14 +3464,15 @@ async function useselectApp(...arguments_) {
3464
3464
 
3465
3465
  Starts one owned browser development server with exact runtime/app route mappings and a caller-selected bind address.
3466
3466
 
3467
- HTTP and HTTPS serving use the published `node-http-server` module. The SDK
3467
+ HTTP and HTTPS serving use published `node-http-server` 10.0.0. The SDK
3468
3468
  selects source routes and supplies generated representations; the module owns
3469
3469
  static-file conditional GET/HEAD handling and response delivery. The SDK
3470
3470
  retains modification dates for its generated representations. Unchanged
3471
3471
  resources can return `304` with no body while changed resources return their
3472
- complete current representation. PEM-path listeners use module deployment;
3473
- explicit raw `tls` options retain native HTTPS transport with the same module
3474
- response and static-file operations.
3472
+ complete current representation. PEM-path listeners use module deployment and
3473
+ negotiate HTTP/2 or HTTP/1.1 on the same HTTPS port. Explicit raw `tls` options
3474
+ retain native HTTPS transport with the same module response and static-file
3475
+ operations.
3475
3476
 
3476
3477
  ### Signature, modes, and result
3477
3478
 
@@ -3518,8 +3519,12 @@ requirements remain browser-owned; see [explicit HTTP development](cli.md#explic
3518
3519
  The promise settles after all selected listeners are ready and resolves to
3519
3520
  `{server, protocol, mode, workspaceRoot, appId, host, port, origin, cleanUrl, url,
3520
3521
  networkUrls, httpPort, httpOrigin, httpUrl, close, closed, lifecycle}`. `server` is the raw Node
3521
- server for application content; `protocol` is `'https:'` by default or `'http:'`
3522
- with explicit HTTP source mode.
3522
+ server for application content: `Http2SecureServer` for PEM-backed HTTPS,
3523
+ `https.Server` for explicit raw `tls` options, or `http.Server` for explicit
3524
+ HTTP source mode. HTTP/2 uses Node's compatibility request/response APIs;
3525
+ callers accessing the raw server must account for its native type.
3526
+ `protocol` describes the URL scheme: `'https:'` for either negotiated HTTPS
3527
+ protocol or `'http:'` with explicit HTTP source mode.
3523
3528
  `url` and `cleanUrl` are the same application URL. Wildcard listeners use
3524
3529
  `localhost` in that local URL; `host` retains the actual bound address.
3525
3530
  `httpPort` is the actual HTTP listener port, `httpOrigin` is its HTTP origin,
@@ -159,6 +159,7 @@ The upstream listener and public lifecycle follow.
159
159
  | `deployMailServer` | Adapts published `deploy(callback)` into listener startup promise. | Y/Y/N — Keep. | Startup must return the actual bound address or an actual bind error. Current name: `listenForMailRequests`. |
160
160
  | `waitForMailListener` | Owns the startup promise. | Y/Y/N — Keep. | Required callback-to-promise adapter at the upstream owner. Name is clear. |
161
161
  | Listener `onError` | Rejects startup when binding fails. | Y/Y/N — Keep. | Do not claim listening before this boundary. Current callback name remains `onError`, local to the listener promise. |
162
+ | Occupied-port startup message | `startResendMailServer` handles the actual native `EADDRINUSE` after closing its failed launch resources. | Y/Y/N — Keep. | Identifies the selected mail port and says another mail server may own it. The existing CLI failure path exits with status 1; native error metadata and the complete original cause remain available. One failure-only branch replaces the generic bind message, with no port probe, retry, process control, or work on successful requests. Source and diff reviewed; this follow-up was not exercised locally. |
162
163
  | `onListening` | Removes startup error listener and resolves actual raw server. | Y/Y/N — Keep. | Retain upstream callback semantics. Current callback name remains `onListening`; its `(instance, server)` positions follow the upstream callback. |
163
164
  | `startResendMailServer` | Constructs upstream server and mail handler, then exposes lifecycle. | Y/Y/Y — Keep entry point; remove duplicate configuration/loopback gate. | Use configured host/domain; keep selected upstream public interface and no implicit socket deadline. Preserve public name. |
164
165
  | `handleMailRequest` assigned to `onRawRequest` | Hands original Node request/response to mail handler and returns true. | Y/Y/N — Keep. | `true` prevents upstream body/static processing of an already-owned mail request. Current name: `routeRawMailRequest`, distinct from the actual request handler. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.16.3",
3
+ "version": "0.18.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -68,7 +68,7 @@
68
68
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
69
69
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
70
70
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
71
- "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
71
+ "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
72
72
  "test:integration": "node ./bin/arcane-test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
73
73
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
74
74
  "check": "node tools/check-source.mjs && npm test",
@@ -106,7 +106,7 @@
106
106
  },
107
107
  "dependencies": {
108
108
  "event-pubsub": "6.1.0",
109
- "node-http-server": "9.1.1",
109
+ "node-http-server": "10.0.0",
110
110
  "strong-type": "2.0.1",
111
111
  "vanilla-test": "2.1.3"
112
112
  },
@@ -150,7 +150,7 @@
150
150
  <button id="close" class="modal-button" part="close" type="button" aria-label="Close" title="Close">&#10005;</button>
151
151
  <header id="modal-header" class="modal-header" part="header" hidden><slot id="header-slot" name="header"></slot></header>
152
152
  <input type="search" id="modal-search" class="modal-search hidden" part="search" placeholder="Search..." aria-label="Search modal content">
153
- <div id="modal-content" class="modal-content" part="body"><slot name="body"></slot></div>
153
+ <div id="modal-content" class="modal-content" part="body" tabindex="-1"><slot name="body"></slot></div>
154
154
  <footer id="modal-footer" class="modal-footer" part="footer" hidden><slot id="footer-slot" name="footer"></slot></footer>
155
155
  </div>
156
156
  </dialog>
@@ -174,11 +174,13 @@
174
174
  const modalStack=window.modalStack=window.modalStack||[];
175
175
 
176
176
  let running=false;
177
+ let dismissible = true;
177
178
  let disposed=false;
178
179
  let activeRun=Promise.resolve([]);
179
180
  let returnFocus=null;
180
181
 
181
182
  host.ready=false;
183
+ host.configure = configure;
182
184
  host.populate=populate;
183
185
  host.open=open;
184
186
  host.close=close;
@@ -195,13 +197,18 @@
195
197
  opened:{get:function getOpenedState(){return dialog.open;}}
196
198
  });
197
199
 
198
- dialog.addEventListener('cancel',function preventRunningDialogCancellation(event){
199
- event.preventDefault();
200
- close(event);
201
- });
200
+ dialog.addEventListener(
201
+ 'cancel',
202
+ function handleDialogCancellation(event) {
203
+ event.preventDefault();
204
+ if (dismissible) {
205
+ close(event);
206
+ }
207
+ }
208
+ );
202
209
 
203
210
  dialog.addEventListener('click',function closeFromDialogBackdrop(event){
204
- if(event.target!==dialog||running){
211
+ if (event.target !== dialog || running || !dismissible) {
205
212
  return;
206
213
  }
207
214
  const bounds=dialog.getBoundingClientRect();
@@ -214,9 +221,45 @@
214
221
  }
215
222
  });
216
223
 
217
- closeButton.addEventListener('click',function closeFromButton(event){
218
- close(event);
219
- });
224
+ closeButton.addEventListener(
225
+ 'click',
226
+ function closeFromButton(event) {
227
+ if (dismissible) {
228
+ close(event);
229
+ }
230
+ }
231
+ );
232
+
233
+ function configure(input = {}) {
234
+ if (disposed) return false;
235
+ if (input === null || !is.object(input) || is.array(input)) {
236
+ throw new TypeError('Modal configuration must be an object.');
237
+ }
238
+ if (input.dismissible !== undefined) {
239
+ if (!is.boolean(input.dismissible)) {
240
+ throw new TypeError('Modal dismissible must be a boolean.');
241
+ }
242
+ dismissible = input.dismissible;
243
+ }
244
+ const closeButtonFocused = shadowRoot.activeElement === closeButton;
245
+ updateCloseButton();
246
+ if (dialog.open && !dismissible && closeButtonFocused) {
247
+ focusOpenedModal();
248
+ }
249
+ return {dismissible};
250
+ }
251
+
252
+ function updateCloseButton() {
253
+ closeButton.classList.toggle('hidden', running || !dismissible);
254
+ }
255
+
256
+ function focusOpenedModal() {
257
+ const focusTarget = modalContent.querySelector('[autofocus],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),a[href]')
258
+ || (running || !dismissible ? modalContent : closeButton);
259
+ focusTarget.focus(
260
+ {preventScroll: true}
261
+ );
262
+ }
220
263
 
221
264
  for(const slot of [headerSlot,footerSlot]){
222
265
  const region=slot.parentElement;
@@ -253,10 +296,7 @@
253
296
  ?document.activeElement
254
297
  :null;
255
298
  dialog.showModal();
256
- queueMicrotask(function focusOpenedModal(){
257
- const focusTarget=modalContent.querySelector('[autofocus],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),a[href]')||closeButton;
258
- focusTarget?.focus({preventScroll:true});
259
- });
299
+ queueMicrotask(focusOpenedModal);
260
300
  emit('modal-opened',{}, {opened:true});
261
301
  }
262
302
  return true;
@@ -274,7 +314,7 @@
274
314
  if(wasOpen){
275
315
  dialog.close();
276
316
  }
277
- closeButton.classList.remove('hidden');
317
+ updateCloseButton();
278
318
 
279
319
  const stackIndex=modalStack.indexOf(host);
280
320
  if(stackIndex>-1){
@@ -317,7 +357,7 @@
317
357
  }
318
358
 
319
359
  async function populate(content='<p>Content goes here</p>',search=false){
320
- closeButton.classList.remove('hidden');
360
+ updateCloseButton();
321
361
  modalSearch.classList.toggle('hidden',!search);
322
362
  if(content instanceof Node){
323
363
  modalContent.replaceChildren(content);
@@ -349,7 +389,7 @@
349
389
  }
350
390
 
351
391
  running=true;
352
- closeButton.classList.add('hidden');
392
+ updateCloseButton();
353
393
  modalSearch.classList.add('hidden');
354
394
  modalContent.replaceChildren();
355
395
 
@@ -399,16 +439,20 @@
399
439
  return result.status==='rejected';
400
440
  });
401
441
  running=false;
402
- closeButton.classList.remove('hidden');
442
+ updateCloseButton();
403
443
 
404
444
  if(failed){
405
445
  heading.innerText='Some Tasks Could Not Be Completed';
406
- message.innerText='You may close this message and try again.';
446
+ message.innerText = dismissible
447
+ ? 'You may close this message and try again.'
448
+ : 'Review the failed tasks and try again.';
407
449
  return results;
408
450
  }
409
451
 
410
452
  heading.innerText='All Tasks Complete';
411
- message.innerText='Every task completed. You may close this message.';
453
+ message.innerText = dismissible
454
+ ? 'Every task completed. You may close this message.'
455
+ : 'Every task completed.';
412
456
  return results;
413
457
  }
414
458
 
@@ -1033,6 +1033,15 @@ export async function startResendMailServer(options={}){
1033
1033
  server=await listenForMailRequests(mailServer);
1034
1034
  }catch(error){
1035
1035
  await Promise.allSettled([mailServer.close(),requestHandler.close()]);
1036
+ if (error?.code === 'EADDRINUSE') {
1037
+ throw Object.assign(
1038
+ new Error(
1039
+ `Mail port ${configuration.port} is already taken, possibly by another mail server.`,
1040
+ {cause:error}
1041
+ ),
1042
+ error
1043
+ );
1044
+ }
1036
1045
  throw error;
1037
1046
  }
1038
1047
  const address=server.address();