arcane-os 0.2.3 → 0.3.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
  4. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
  5. package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
  6. package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
  8. package/browser-runtime/ai/browser-speech.mjs +2 -0
  9. package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
  10. package/browser-runtime/ai/model-controller.mjs +285 -95
  11. package/browser-runtime/ai/speech-worker-client.mjs +247 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
  13. package/browser-runtime/event-manager.mjs +1097 -1
  14. package/docs/architecture.md +2 -2
  15. package/docs/event-manager.md +155 -27
  16. package/docs/reference/README.md +20 -20
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1162 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +2 -2
  21. package/docs/reference/behavioral-testing.md +28 -5
  22. package/docs/reference/cli.md +115 -7
  23. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  24. package/docs/reference/event-manager.md +577 -32
  25. package/docs/reference/inventory/package-api.json +478 -2
  26. package/docs/reference/inventory/runtime-components.json +86 -37
  27. package/docs/reference/inventory/runtime-modules.json +126 -49
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +143 -33
  30. package/docs/reference/runtime-components.md +192 -80
  31. package/docs/reference/runtime-modules.md +584 -70
  32. package/docs/reference/sdk-api.md +1014 -25
  33. package/package.json +5 -4
  34. package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
  35. package/runtime/arcane/components/app-bar.html +34 -13
  36. package/runtime/arcane/components/assistant-panel.html +110 -57
  37. package/runtime/arcane/components/calculator.html +7 -4
  38. package/runtime/arcane/components/chart.html +58 -17
  39. package/runtime/arcane/components/chat.html +606 -136
  40. package/runtime/arcane/components/conversation-view.html +13 -6
  41. package/runtime/arcane/components/dashboard-config.html +96 -59
  42. package/runtime/arcane/components/data-maintenance.html +69 -14
  43. package/runtime/arcane/components/data-view.html +53 -7
  44. package/runtime/arcane/components/directory-picker.html +118 -32
  45. package/runtime/arcane/components/document-inspector.html +47 -10
  46. package/runtime/arcane/components/file-drop.html +81 -35
  47. package/runtime/arcane/components/file-inspector.html +72 -22
  48. package/runtime/arcane/components/file-manager.html +374 -79
  49. package/runtime/arcane/components/integration-settings.html +12 -5
  50. package/runtime/arcane/components/local-ai-status.html +48 -19
  51. package/runtime/arcane/components/markdown-document.html +161 -68
  52. package/runtime/arcane/components/markdown-editor.html +110 -33
  53. package/runtime/arcane/components/media-embed.html +8 -5
  54. package/runtime/arcane/components/modal.html +15 -5
  55. package/runtime/arcane/components/output-panel.html +28 -23
  56. package/runtime/arcane/components/preferences-form.html +22 -4
  57. package/runtime/arcane/components/record-timeline.html +18 -2
  58. package/runtime/arcane/components/relationship-board.html +23 -3
  59. package/runtime/arcane/components/screen-capture.html +10 -4
  60. package/runtime/arcane/components/source-code-viewer.html +76 -9
  61. package/runtime/arcane/components/source-explanation.html +23 -3
  62. package/runtime/arcane/components/speech.html +453 -131
  63. package/runtime/arcane/components/summary-strip.html +22 -11
  64. package/runtime/arcane/components/table.html +39 -21
  65. package/runtime/arcane/components/task-progress.html +79 -21
  66. package/runtime/arcane/components/terminal-workspace.html +7 -4
  67. package/runtime/arcane/components/theme-editor.html +7 -3
  68. package/runtime/arcane/components/unified-inbox.html +9 -4
  69. package/runtime/arcane/components/voice-transcription.html +244 -72
  70. package/runtime/arcane/components/weather-widget.html +5 -3
  71. package/runtime/arcane/components/web-navigator.html +48 -8
  72. package/runtime/arcane/entities/Chat.js +1 -1
  73. package/runtime/arcane/entities/User.js +110 -23
  74. package/runtime/arcane/modules/AI.js +2109 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
  76. package/runtime/arcane/modules/AIRuntimeState.js +109 -52
  77. package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
  78. package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
  79. package/runtime/arcane/modules/CalculatorEngine.js +63 -3
  80. package/runtime/arcane/modules/CommunicationAppController.js +588 -28
  81. package/runtime/arcane/modules/CommunicationHub.js +590 -10
  82. package/runtime/arcane/modules/ComponentContracts.js +235 -37
  83. package/runtime/arcane/modules/ConversationTimebox.js +152 -33
  84. package/runtime/arcane/modules/DBLS.js +40 -7
  85. package/runtime/arcane/modules/DBOPFS.js +35 -11
  86. package/runtime/arcane/modules/DataMaintenance.js +12 -2
  87. package/runtime/arcane/modules/Errors.js +65 -7
  88. package/runtime/arcane/modules/HTMLImport.js +198 -14
  89. package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
  90. package/runtime/arcane/modules/Mail.js +738 -115
  91. package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
  92. package/runtime/arcane/modules/MailTransport.mjs +197 -39
  93. package/runtime/arcane/modules/Ollama.js +36 -1
  94. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
  95. package/runtime/arcane/modules/PreferenceStore.js +367 -33
  96. package/runtime/arcane/modules/RecordReviewStore.js +322 -23
  97. package/runtime/arcane/modules/ScreenCapture.js +1397 -15
  98. package/runtime/arcane/modules/SpeechPlayback.js +438 -41
  99. package/runtime/arcane/modules/TerminalClient.js +277 -12
  100. package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
  101. package/runtime/arcane/modules/ThemeManager.js +39 -7
  102. package/runtime/arcane/modules/TimeGuard.js +131 -20
  103. package/runtime/arcane/modules/WaitForComponent.js +386 -33
  104. package/schemas/arcane-lock.schema.json +2 -2
  105. package/src/cli/main.mjs +435 -9
  106. package/src/event-manager.mjs +1097 -1
  107. package/src/import-map.mjs +21 -3
  108. package/src/index.mjs +13 -0
  109. package/src/installed-sdk-runtime.mjs +112 -0
  110. package/src/mail-api.mjs +22 -0
  111. package/src/mail-credentials.mjs +667 -0
  112. package/src/mail-server.mjs +1769 -0
  113. package/src/mail.mjs +261 -0
  114. package/src/sdk-browser-runtime.mjs +85 -41
  115. package/src/testing-loader.mjs +7 -0
  116. package/src/toolchain.mjs +3 -0
  117. package/src/workspace.mjs +1 -1
@@ -2,8 +2,22 @@ import {
2
2
  APP_LOCAL_STORAGE_PREFIX,
3
3
  resolveBrowserApplicationId
4
4
  } from './AppDataScope.js';
5
+ import {
6
+ createArcaneEventSource,
7
+ projectArcaneDOMEvent
8
+ } from 'arcane-os/event-manager';
9
+
10
+ const PUBLISH_DBLS_READY=Symbol('publish-dbls-ready');
11
+ export const DBLS_EVENT_TYPES=Object.freeze({
12
+ ready:'dbls-ready'
13
+ });
14
+ export const DBLS_REASONS=Object.freeze({
15
+ ready:'local-storage-adapter-ready'
16
+ });
5
17
 
6
18
  class DBLS {
19
+ #events;
20
+
7
21
  constructor({
8
22
  applicationId=null,
9
23
  documentObject=globalThis.document,
@@ -27,6 +41,10 @@ class DBLS {
27
41
  });
28
42
  this.storage=storage;
29
43
  this.storagePrefix=`${APP_LOCAL_STORAGE_PREFIX}${this.applicationId}:`;
44
+ this.#events=createArcaneEventSource(this,{
45
+ source:'dbls',
46
+ eventTypes:Object.freeze(Object.values(DBLS_EVENT_TYPES))
47
+ });
30
48
  }
31
49
 
32
50
  ready=false;
@@ -153,19 +171,34 @@ class DBLS {
153
171
  count() {
154
172
  return this.logicalKeys().length;
155
173
  }
174
+
175
+ [PUBLISH_DBLS_READY](){
176
+ const detail=Object.freeze({
177
+ dbls:this,
178
+ applicationId:this.applicationId,
179
+ reason:DBLS_REASONS.ready
180
+ });
181
+ const {occurrence}=this.#events.dispatch(
182
+ DBLS_EVENT_TYPES.ready,
183
+ detail,
184
+ {
185
+ operationId:`dbls-ready-${this.#events.instanceId}`,
186
+ publicDetail:Object.freeze({
187
+ applicationId:this.applicationId,
188
+ ready:true,
189
+ reason:DBLS_REASONS.ready
190
+ })
191
+ }
192
+ );
193
+ projectArcaneDOMEvent(window,occurrence);
194
+ }
156
195
  }
157
196
 
158
197
  if(typeof window.dbls?.get !== "function"){
159
198
  window.dbls=new DBLS();
160
199
  window.dbls.ready=true;
161
200
 
162
- const dblsReady=new CustomEvent(
163
- 'dbls-ready', {
164
- detail: { dbls: window.dbls }
165
- }
166
- );
167
-
168
- window.dispatchEvent(dblsReady);
201
+ window.dbls[PUBLISH_DBLS_READY]();
169
202
  }
170
203
 
171
204
  export default DBLS;
@@ -1,7 +1,18 @@
1
1
  import Is from "../../node_modules/strong-type/index.js";
2
2
  import {openApplicationDataDirectory} from './AppDataScope.js';
3
+ import {
4
+ createArcaneEventSource,
5
+ projectArcaneDOMEvent
6
+ } from 'arcane-os/event-manager';
3
7
  const is=new Is(false);
4
8
 
9
+ export const DBOPFS_EVENT_TYPES=Object.freeze({
10
+ ready:'dbopfs-ready'
11
+ });
12
+ export const DBOPFS_REASONS=Object.freeze({
13
+ ready:'opfs-database-ready'
14
+ });
15
+
5
16
  const DEFAULT_TABLE_DIRECTORIES=Object.freeze({
6
17
  users:'users',
7
18
  scores:'scores',
@@ -94,6 +105,8 @@ if(navigator.storage?.persist){
94
105
  */
95
106
  class DBOPFS {
96
107
 
108
+ #events;
109
+
97
110
  /** @type {FileSystemDirectoryHandle|Object} */
98
111
  #db={}
99
112
 
@@ -270,6 +283,10 @@ class DBOPFS {
270
283
  return window.dbopfs;
271
284
  }
272
285
 
286
+ this.#events=createArcaneEventSource(this,{
287
+ source:'dbopfs',
288
+ eventTypes:Object.freeze(Object.values(DBOPFS_EVENT_TYPES))
289
+ });
273
290
  this.readyPromise=this.#init(options);
274
291
  }
275
292
 
@@ -294,18 +311,25 @@ class DBOPFS {
294
311
 
295
312
  this.ready=true;
296
313
 
297
- window.dispatchEvent(
298
- new CustomEvent(
299
- 'dbopfs-ready',
300
- {
301
- detail:{
302
- dbopfs:this,
303
- applicationId:this.#applicationId,
304
- storagePath:this.#storagePath
305
- }
306
- }
307
- )
314
+ const {occurrence}=this.#events.dispatch(
315
+ DBOPFS_EVENT_TYPES.ready,
316
+ Object.freeze({
317
+ dbopfs:this,
318
+ applicationId:this.#applicationId,
319
+ storagePath:this.#storagePath,
320
+ reason:DBOPFS_REASONS.ready
321
+ }),
322
+ {
323
+ operationId:`dbopfs-ready-${this.#events.instanceId}`,
324
+ publicDetail:Object.freeze({
325
+ applicationId:this.#applicationId,
326
+ ready:true,
327
+ reason:DBOPFS_REASONS.ready,
328
+ storagePath:this.#storagePath
329
+ })
330
+ }
308
331
  );
332
+ projectArcaneDOMEvent(window,occurrence);
309
333
  }
310
334
 
311
335
  async #createDefaultTables(){
@@ -1,6 +1,7 @@
1
1
  import './DBOPFS.js';
2
2
  import {hasUserEntry} from './ChatRecords.js';
3
3
  import {hasMemoryContent} from './MemoryRecords.js';
4
+ import {arcaneEvents} from 'arcane-os/event-manager';
4
5
 
5
6
  async function clearEmptyChatsAndMemories(){
6
7
  await waitForDBOPFS();
@@ -70,12 +71,21 @@ function waitForDBOPFS(){
70
71
 
71
72
  return new Promise(
72
73
  function waitForDBOPFSPromise(resolve){
74
+ let settled=false;
75
+ let unsubscribe;
73
76
  function ready(){
74
- window.removeEventListener('dbopfs-ready',ready);
77
+ if(settled||window.dbopfs?.ready!==true){
78
+ return;
79
+ }
80
+ settled=true;
81
+ unsubscribe?.();
75
82
  resolve(window.dbopfs);
76
83
  }
77
84
 
78
- window.addEventListener('dbopfs-ready',ready);
85
+ unsubscribe=arcaneEvents.subscribe(
86
+ 'dbopfs-ready',
87
+ ready
88
+ );
79
89
 
80
90
  if(window.dbopfs?.ready){
81
91
  ready();
@@ -1,4 +1,8 @@
1
1
  import waitForComponent from './WaitForComponent.js';
2
+ import {
3
+ arcaneEvents,
4
+ createArcaneEventSource
5
+ } from 'arcane-os/event-manager';
2
6
 
3
7
  const DEFAULT_DELAY_MS=2_000;
4
8
  const DEFAULT_MAX_REPORTS_PER_SESSION=10;
@@ -10,6 +14,19 @@ const MAX_DETAIL_LENGTH=8_000;
10
14
  const HANDLER_MARKER=Symbol.for('arcane.global-errors.handler');
11
15
  const DEVELOPER_MODAL_HREF=new URL('../components/modal.html?v=13',import.meta.url).href;
12
16
 
17
+ export const GLOBAL_ERROR_EVENT_TYPES=Object.freeze({
18
+ browserErrorCaptured:'arcane-error-captured',
19
+ unhandledRejectionCaptured:'arcane-unhandled-rejection-captured'
20
+ });
21
+ export const GLOBAL_ERROR_EVENT_CODES=Object.freeze({
22
+ browserErrorCaptured:'ARCANE_BROWSER_ERROR_CAPTURED',
23
+ unhandledRejectionCaptured:'ARCANE_UNHANDLED_REJECTION_CAPTURED'
24
+ });
25
+ export const GLOBAL_ERROR_REASONS=Object.freeze({
26
+ browserErrorCaptured:'browser-error-captured',
27
+ unhandledRejectionCaptured:'unhandled-promise-rejection-captured'
28
+ });
29
+
13
30
  const MESSAGE_STYLE=[
14
31
  'Write a simple plain-text email showing the error and, when the available details support it, a possible solution.',
15
32
  'If loop_detected or error_storm_detected is true, put that warning first and clearly state that further notifications have been suppressed.',
@@ -374,6 +391,12 @@ async function presentDeveloperIncidentModal(target,incident,fingerprint){
374
391
  }
375
392
 
376
393
  class Errors {
394
+ #events;
395
+
396
+ #operationSequence=0;
397
+
398
+ #stopUserLoaded=null;
399
+
377
400
  constructor(options={}) {
378
401
  const target=options.target||globalThis.window;
379
402
  if(!target||typeof target.addEventListener!=='function'){
@@ -384,6 +407,13 @@ class Errors {
384
407
  return target.errors;
385
408
  }
386
409
 
410
+ this.#events=createArcaneEventSource(this,{
411
+ source:'global-error-handler',
412
+ eventTypes:Object.freeze([
413
+ GLOBAL_ERROR_EVENT_TYPES.browserErrorCaptured,
414
+ GLOBAL_ERROR_EVENT_TYPES.unhandledRejectionCaptured
415
+ ])
416
+ });
387
417
  this[HANDLER_MARKER]=true;
388
418
  this.target=target;
389
419
  this.delayMs=options.delayMs??DEFAULT_DELAY_MS;
@@ -674,17 +704,15 @@ class Errors {
674
704
  }
675
705
 
676
706
  this.waitingForUser=true;
677
- this.target.addEventListener(
707
+ this.#stopUserLoaded=arcaneEvents.subscribe(
678
708
  'user-entity-loaded',
679
- this.onUserLoaded
709
+ this.onUserLoaded,
710
+ {once:true}
680
711
  );
681
712
  }
682
713
 
683
714
  onUserLoaded(){
684
- this.target.removeEventListener(
685
- 'user-entity-loaded',
686
- this.onUserLoaded
687
- );
715
+ this.#stopUserLoaded=null;
688
716
  this.waitingForUser=false;
689
717
 
690
718
  const deferred=this.deferredDeveloperIncident;
@@ -754,6 +782,33 @@ class Errors {
754
782
  return false;
755
783
  }
756
784
 
785
+ const incidentKind=incident?.type==='unhandledrejection'
786
+ ?'unhandled-promise-rejection'
787
+ :'browser-error';
788
+ const eventType=incidentKind==='unhandled-promise-rejection'
789
+ ?GLOBAL_ERROR_EVENT_TYPES.unhandledRejectionCaptured
790
+ :GLOBAL_ERROR_EVENT_TYPES.browserErrorCaptured;
791
+ const code=incidentKind==='unhandled-promise-rejection'
792
+ ?GLOBAL_ERROR_EVENT_CODES.unhandledRejectionCaptured
793
+ :GLOBAL_ERROR_EVENT_CODES.browserErrorCaptured;
794
+ const reason=incidentKind==='unhandled-promise-rejection'
795
+ ?GLOBAL_ERROR_REASONS.unhandledRejectionCaptured
796
+ :GLOBAL_ERROR_REASONS.browserErrorCaptured;
797
+ this.#operationSequence+=1;
798
+ this.#events.dispatch(
799
+ eventType,
800
+ Object.freeze({...incident,code,fingerprint,reason}),
801
+ {
802
+ operationId:`global-error-handler-${this.#events.instanceId}-${this.#operationSequence.toString(36)}`,
803
+ publicDetail:Object.freeze({
804
+ id:fingerprint,
805
+ code,
806
+ kind:incidentKind,
807
+ reason
808
+ })
809
+ }
810
+ );
811
+
757
812
  this.offerDeveloperIncident(incident,fingerprint);
758
813
 
759
814
  if(this.circuitOpen){
@@ -976,7 +1031,8 @@ class Errors {
976
1031
  this.destroyed=true;
977
1032
  this.target.removeEventListener('error',this.onError,true);
978
1033
  this.target.removeEventListener('unhandledrejection',this.onRejection,true);
979
- this.target.removeEventListener('user-entity-loaded',this.onUserLoaded);
1034
+ this.#stopUserLoaded?.();
1035
+ this.#stopUserLoaded=null;
980
1036
  this.waitingForUser=false;
981
1037
  this.deferredDeveloperIncident=null;
982
1038
 
@@ -1015,6 +1071,8 @@ class Errors {
1015
1071
  this.target.errors=undefined;
1016
1072
  }
1017
1073
  }
1074
+
1075
+ this.#events.dispose();
1018
1076
  }
1019
1077
  }
1020
1078
 
@@ -1,3 +1,8 @@
1
+ import {
2
+ createArcaneEventSource,
3
+ projectArcaneDOMEvent
4
+ } from 'arcane-os/event-manager';
5
+
1
6
  const htmlImportHostRegistryKey=Symbol.for('arcane.html-import.hosts');
2
7
  const htmlImportHostRegistry=globalThis[htmlImportHostRegistryKey] instanceof Map
3
8
  ?globalThis[htmlImportHostRegistryKey]
@@ -7,7 +12,24 @@ globalThis[htmlImportHostRegistryKey]=htmlImportHostRegistry;
7
12
 
8
13
  let htmlImportScriptId=0;
9
14
 
15
+ function samePropertyDescriptor(left,right){
16
+ if(!left||!right)return left===right;
17
+ return left.configurable===right.configurable
18
+ &&left.enumerable===right.enumerable
19
+ &&left.writable===right.writable
20
+ &&left.value===right.value
21
+ &&left.get===right.get
22
+ &&left.set===right.set;
23
+ }
24
+
10
25
  class HTMLImport extends HTMLElement {
26
+ #connectionGeneration=0;
27
+ #eventOwner=Object.freeze({kind:'html-import-lifecycle-owner'});
28
+ #events;
29
+ #importedDestroy=null;
30
+ #loadController=null;
31
+ #loadTask=Promise.resolve();
32
+
11
33
  ready=false;
12
34
 
13
35
  constructor() {
@@ -15,12 +37,40 @@ class HTMLImport extends HTMLElement {
15
37
  this.attachShadow({ mode: 'open' });
16
38
  }
17
39
 
18
- async connectedCallback() {
40
+ #createEvents(){
41
+ this.#events=createArcaneEventSource(this.#eventOwner,{
42
+ source:'html-import',
43
+ eventTypes:Object.freeze([
44
+ 'html-import-error',
45
+ 'html-import-ready'
46
+ ])
47
+ });
48
+ return this.#events;
49
+ }
50
+
51
+ connectedCallback() {
52
+ const generation=++this.#connectionGeneration;
53
+ const controller=new AbortController();
19
54
  this.ready=false;
55
+ this.#loadController?.abort('html-import-load-superseded');
56
+ this.#loadController=controller;
57
+ const previous=this.#loadTask;
58
+ const task=Promise.resolve(previous)
59
+ .catch(()=>{})
60
+ .then(()=>this.#connect(generation,controller));
61
+ this.#loadTask=task;
62
+ return task;
63
+ }
64
+
65
+ async #connect(generation,controller){
66
+ if(!this.#isCurrentConnection(generation,controller))return;
67
+ if(!this.#events||this.#events.disposed)this.#createEvents();
20
68
  const href=this.getAttribute('href');
21
69
  let resolvedHref='';
70
+ const operationId=`html-import-load-${this.#events.instanceId}-${generation}`;
22
71
  if(!href){
23
72
  console.log('no href provided for html-import tag',this);
73
+ this.#loadController=null;
24
74
  return;
25
75
  }
26
76
 
@@ -35,46 +85,167 @@ class HTMLImport extends HTMLElement {
35
85
  cache:'default',
36
86
  credentials:'same-origin',
37
87
  method:'GET',
38
- redirect:'error'
88
+ redirect:'error',
89
+ signal:controller.signal
39
90
  });
40
- await this.#loadHTML(href,resolvedHref,response);
91
+ if(!this.#isCurrentConnection(generation,controller))return;
92
+ await this.#loadHTML(href,resolvedHref,response,generation,controller,operationId);
41
93
  }catch(err){
94
+ if(!this.#isCurrentConnection(generation,controller)){
95
+ await this.#destroyImportedHost();
96
+ return;
97
+ }
42
98
  console.error('Error loading HTML component:',err);
43
- this.dispatchEvent(new CustomEvent('html-import-error',{
44
- bubbles:true,
45
- composed:true,
46
- detail:{
99
+ const detail=Object.freeze({
47
100
  code:'HTML_IMPORT_FAILED',
48
101
  href,
49
102
  message:'The component could not be loaded.',
103
+ reason:'component-import-rejected',
50
104
  resolvedHref
105
+ });
106
+ const {occurrence}=this.#events.dispatch(
107
+ 'html-import-error',
108
+ detail,
109
+ {
110
+ operationId,
111
+ publicDetail:Object.freeze({code:detail.code,reason:detail.reason})
51
112
  }
52
- }));
113
+ );
114
+ projectArcaneDOMEvent(this,occurrence,{
115
+ bubbles:true,
116
+ composed:true
117
+ });
118
+ await this.#destroyImportedHost();
119
+ }finally{
120
+ if(this.#loadController===controller)this.#loadController=null;
121
+ }
122
+ }
123
+
124
+ disconnectedCallback(){
125
+ this.ready=false;
126
+ ++this.#connectionGeneration;
127
+ this.#loadController?.abort('html-import-disconnected');
128
+ this.#loadController=null;
129
+ const events=this.#events;
130
+ const previous=this.#loadTask;
131
+ const immediateTeardown=this.#destroyImportedHost();
132
+ const task=Promise.allSettled([previous,immediateTeardown]).then(()=>{
133
+ events?.dispose();
134
+ if(this.#events===events)this.#events=null;
135
+ });
136
+ this.#loadTask=task;
137
+ return task;
138
+ }
139
+
140
+ #isCurrentConnection(generation,controller){
141
+ return this.isConnected
142
+ &&generation===this.#connectionGeneration
143
+ &&this.#loadController===controller
144
+ &&!controller.signal.aborted;
145
+ }
146
+
147
+ #reportImportedDestroyError(error,{events,href,operationId}){
148
+ console.error('Error destroying imported HTML component:',error);
149
+ if(!events||events.disposed)return;
150
+ const detail=Object.freeze({
151
+ code:'HTML_IMPORT_IMPORTED_COMPONENT_DESTROY_REJECTED',
152
+ href,
153
+ message:'The imported component could not be destroyed.',
154
+ reason:'imported-component-destroy-rejected'
155
+ });
156
+ const {occurrence}=events.dispatch(
157
+ 'html-import-error',
158
+ detail,
159
+ {
160
+ operationId,
161
+ publicDetail:Object.freeze({code:detail.code,reason:detail.reason})
162
+ }
163
+ );
164
+ projectArcaneDOMEvent(this,occurrence,{
165
+ bubbles:true,
166
+ composed:true
167
+ });
168
+ }
169
+
170
+ #restoreImportedDestroy(record){
171
+ const current=Object.getOwnPropertyDescriptor(this,'destroy')??null;
172
+ if(!samePropertyDescriptor(current,record.installedDescriptor))return false;
173
+ if(record.previousDescriptor){
174
+ Object.defineProperty(this,'destroy',record.previousDescriptor);
175
+ }else{
176
+ delete this.destroy;
177
+ }
178
+ return true;
179
+ }
180
+
181
+ #destroyImportedHost(){
182
+ const record=this.#importedDestroy;
183
+ if(!record)return Promise.resolve(false);
184
+ this.#importedDestroy=null;
185
+ const events=this.#events;
186
+ const generation=this.#connectionGeneration;
187
+ const reportContext={
188
+ events,
189
+ href:this.getAttribute('href')||'',
190
+ operationId:events
191
+ ?`html-import-destroy-${events.instanceId}-${generation}`
192
+ :null
193
+ };
194
+ let result;
195
+ try{
196
+ result=record.destroy.call(this);
197
+ }catch(error){
198
+ this.#reportImportedDestroyError(error,reportContext);
199
+ this.#restoreImportedDestroy(record);
200
+ return Promise.resolve(false);
53
201
  }
202
+ return Promise.resolve(result)
203
+ .then(
204
+ ()=>true,
205
+ error=>{
206
+ this.#reportImportedDestroyError(error,reportContext);
207
+ return false;
208
+ }
209
+ )
210
+ .finally(()=>this.#restoreImportedDestroy(record));
54
211
  }
55
212
 
56
- async #loadHTML(href,resolvedHref,response){
213
+ async #loadHTML(href,resolvedHref,response,generation,controller,operationId){
57
214
  if (!response.ok) {
58
215
  throw new Error(`HTTP error! Status: ${response.status}`);
59
216
  }
60
217
 
61
218
  const html = await response.text();
219
+ if(!this.#isCurrentConnection(generation,controller))return false;
220
+ await this.#destroyImportedHost();
221
+ if(!this.#isCurrentConnection(generation,controller))return false;
62
222
  this.shadowRoot.innerHTML = html;
63
223
 
64
224
  await this.#executeScripts();
225
+ if(!this.#isCurrentConnection(generation,controller)){
226
+ await this.#destroyImportedHost();
227
+ return false;
228
+ }
65
229
 
66
230
  this.ready=true;
67
- this.dispatchEvent(new CustomEvent('html-import-ready',{
231
+ const {occurrence}=this.#events.dispatch(
232
+ 'html-import-ready',
233
+ Object.freeze({href,resolvedHref}),
234
+ {operationId,publicDetail:Object.freeze({ready:true})}
235
+ );
236
+ projectArcaneDOMEvent(this,occurrence,{
68
237
  bubbles:true,
69
- composed:true,
70
- detail:{href,resolvedHref}
71
- }));
238
+ composed:true
239
+ });
72
240
  return true;
73
241
  }
74
242
 
75
243
  async #executeScripts() {
76
244
  const scripts = Array.from(this.shadowRoot.querySelectorAll('script'));
77
- for(const script of scripts){
245
+ const previousDescriptor=Object.getOwnPropertyDescriptor(this,'destroy')??null;
246
+ const previousDestroy=this.destroy;
247
+ try{
248
+ for(const script of scripts){
78
249
  if (script.src) {
79
250
  console.error('ONLY INLINE SCRIPTS SUPPORTED AT THIS TIME FOR SECURITY REASONS');
80
251
  console.warn('script src path will need to be limited to ./{text}.js, ../{text}.js), or acceptable sub folders. This can be complex.');
@@ -105,6 +276,19 @@ class HTMLImport extends HTMLElement {
105
276
  htmlImportHostRegistry.delete(hostToken);
106
277
  delete executable.dataset.arcaneHostToken;
107
278
  }
279
+ }
280
+ }finally{
281
+ const installedDescriptor=Object.getOwnPropertyDescriptor(this,'destroy')??null;
282
+ const installedDestroy=this.destroy;
283
+ if(typeof installedDestroy==='function'
284
+ &&installedDestroy!==previousDestroy
285
+ &&!samePropertyDescriptor(installedDescriptor,previousDescriptor)){
286
+ this.#importedDestroy={
287
+ destroy:installedDestroy,
288
+ installedDescriptor,
289
+ previousDescriptor
290
+ };
291
+ }
108
292
  }
109
293
  }
110
294
  }