arcane-os 0.2.2 → 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 +29 -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 +27 -27
  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 +6 -3
  21. package/docs/reference/behavioral-testing.md +29 -6
  22. package/docs/reference/cli.md +117 -9
  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 +108 -44
  27. package/docs/reference/inventory/runtime-modules.json +131 -53
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +157 -43
  30. package/docs/reference/runtime-components.md +258 -83
  31. package/docs/reference/runtime-modules.md +613 -77
  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 +462 -384
  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 +639 -98
  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 +470 -0
  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
@@ -234,6 +234,9 @@
234
234
  <script type="module">
235
235
  const host=this;
236
236
  const root=host.shadowRoot;
237
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
238
+ const events=createArcaneEventSource(host,{source:'arcane.component.app-bar',eventTypes:['app-bar-ready']});
239
+ const lifecycleController=new AbortController();
237
240
  const nav=root.querySelector('nav');
238
241
  const brandLink=root.querySelector('#brandLink');
239
242
  const navigationSlot=root.querySelector('#navigationSlot');
@@ -242,11 +245,13 @@
242
245
  const statusTones=new Set(['default','success','warning','error','muted']);
243
246
 
244
247
  let navigation=[];
248
+ let destroyed=false;
245
249
 
246
250
  host.setNavigation=setNavigation;
247
251
  host.setActiveRoute=setActiveRoute;
248
252
  host.setStatus=setStatus;
249
253
  host.refresh=refresh;
254
+ host.destroy=destroy;
250
255
 
251
256
  Object.defineProperties(
252
257
  host,
@@ -269,9 +274,9 @@
269
274
  }
270
275
  );
271
276
 
272
- navigationSlot.addEventListener('slotchange',updateCurrent);
273
- window.addEventListener('hashchange',onLocationChange);
274
- window.addEventListener('popstate',onLocationChange);
277
+ navigationSlot.addEventListener('slotchange',updateCurrent,{signal:lifecycleController.signal});
278
+ window.addEventListener('hashchange',onLocationChange,{signal:lifecycleController.signal});
279
+ window.addEventListener('popstate',onLocationChange,{signal:lifecycleController.signal});
275
280
 
276
281
  const observer=new MutationObserver(
277
282
  records=>{
@@ -322,6 +327,9 @@
322
327
  }
323
328
 
324
329
  function setNavigation(items=[]){
330
+ if(destroyed){
331
+ return false;
332
+ }
325
333
  navigation=normalizeNavigation(items);
326
334
  generatedNavigation.replaceChildren(
327
335
  ...navigation.map(
@@ -395,6 +403,9 @@
395
403
  }
396
404
 
397
405
  function setActiveRoute(route=''){
406
+ if(destroyed){
407
+ return false;
408
+ }
398
409
  if(route){
399
410
  host.setAttribute('data-active-route',String(route).replace(/^#/,''));
400
411
  }else{
@@ -424,6 +435,9 @@
424
435
  }
425
436
 
426
437
  function setStatus(message='',tone='default'){
438
+ if(destroyed){
439
+ return false;
440
+ }
427
441
  if(message===null||message===''){
428
442
  host.removeAttribute('data-status');
429
443
  }else{
@@ -443,26 +457,33 @@
443
457
  }
444
458
 
445
459
  function refresh(){
460
+ if(destroyed){
461
+ return false;
462
+ }
446
463
  syncBrand();
447
464
  syncStatus();
448
465
  updateCurrent();
449
466
  return host;
450
467
  }
451
468
 
469
+ function destroy(){
470
+ if(destroyed){
471
+ return false;
472
+ }
473
+ destroyed=true;
474
+ lifecycleController.abort();
475
+ observer.disconnect();
476
+ events.dispose();
477
+ host.ready=false;
478
+ return true;
479
+ }
480
+
452
481
  syncBrand();
453
482
  syncStatus();
454
483
  readNavigationAttribute();
455
484
  updateCurrent();
456
485
 
457
486
  host.ready=true;
458
- host.dispatchEvent(
459
- new CustomEvent(
460
- 'app-bar-ready',
461
- {
462
- detail:{appBar:host},
463
- bubbles:true,
464
- composed:true
465
- }
466
- )
467
- );
487
+ const readyPublication=events.dispatch('app-bar-ready',{appBar:host},{publicDetail:{ready:true}});
488
+ projectArcaneDOMEvent(host,readyPublication.occurrence,{bubbles:true,composed:true});
468
489
  </script>
@@ -300,6 +300,9 @@
300
300
  <script type="module">
301
301
  const host=this;
302
302
  const root=host.shadowRoot;
303
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
304
+ const events=createArcaneEventSource(host,{source:'arcane.component.assistant-panel',eventTypes:['assistant-opened','assistant-closed','assistant-send','assistant-clear','assistant-ready']});
305
+ const lifecycleController=new AbortController();
303
306
  const shell=root.querySelector('.assistant-shell');
304
307
  const panel=root.querySelector('.panel');
305
308
  const stateRegion=root.querySelector('.state');
@@ -311,10 +314,18 @@
311
314
  const backdrop=root.querySelector('.backdrop');
312
315
  const validStates=new Set(['ready','empty','pending','streaming','error']);
313
316
  const handledSubmits=new WeakSet();
314
- const pendingDisabledState=new WeakMap();
317
+ const pendingDisabledState=new Map();
315
318
 
316
319
  let previousFocus=null;
317
320
  let opened=host.hasAttribute('data-open');
321
+ let operationSequence=0;
322
+ let destroyed=false;
323
+ const reservedProjectionKeys=Object.freeze([
324
+ 'occurrenceId',
325
+ 'arcaneSource',
326
+ 'instanceId',
327
+ 'operationId'
328
+ ]);
318
329
 
319
330
  host.open=open;
320
331
  host.close=close;
@@ -324,6 +335,29 @@
324
335
  host.setState=setState;
325
336
  host.focusComposer=focusComposer;
326
337
  host.scrollToEnd=scrollToEnd;
338
+ host.destroy=destroy;
339
+
340
+ function nextOperationId(kind){
341
+ operationSequence+=1;
342
+ return `${events.descriptor.instanceId}:${kind}:${operationSequence}`;
343
+ }
344
+
345
+ function compatibilityDetail(input={}){
346
+ const detail={...input};
347
+ for(const key of reservedProjectionKeys){
348
+ delete detail[key];
349
+ }
350
+ return detail;
351
+ }
352
+
353
+ function emit(type,detail={},publicDetail={},options={}){
354
+ if(destroyed){
355
+ return false;
356
+ }
357
+ const publication=events.dispatch(type,detail,{publicDetail,operationId:options.operationId||null,cancelable:Boolean(options.cancelable)});
358
+ if(options.cancelable&&!publication.accepted) return false;
359
+ return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true,cancelable:Boolean(options.cancelable)});
360
+ }
327
361
 
328
362
  Object.defineProperties(
329
363
  host,
@@ -349,15 +383,15 @@
349
383
  }
350
384
  );
351
385
 
352
- closeButton.addEventListener('click',()=>close());
353
- clearButton.addEventListener('click',()=>clear());
354
- backdrop.addEventListener('click',()=>close());
355
- root.addEventListener('submit',onComposerSubmit);
356
- host.addEventListener('submit',onComposerSubmit);
357
- host.addEventListener('click',onHostAction);
358
- document.addEventListener('keydown',onDocumentKeydown);
359
- messagesSlot.addEventListener('slotchange',syncState);
360
- composerSlot.addEventListener('slotchange',syncComposerState);
386
+ closeButton.addEventListener('click',()=>close(),{signal:lifecycleController.signal});
387
+ clearButton.addEventListener('click',()=>clear(),{signal:lifecycleController.signal});
388
+ backdrop.addEventListener('click',()=>close(),{signal:lifecycleController.signal});
389
+ root.addEventListener('submit',onComposerSubmit,{signal:lifecycleController.signal});
390
+ host.addEventListener('submit',onComposerSubmit,{signal:lifecycleController.signal});
391
+ host.addEventListener('click',onHostAction,{signal:lifecycleController.signal});
392
+ document.addEventListener('keydown',onDocumentKeydown,{signal:lifecycleController.signal});
393
+ messagesSlot.addEventListener('slotchange',syncState,{signal:lifecycleController.signal});
394
+ composerSlot.addEventListener('slotchange',syncComposerState,{signal:lifecycleController.signal});
361
395
 
362
396
  const observer=new MutationObserver(
363
397
  records=>{
@@ -400,6 +434,9 @@
400
434
  }
401
435
 
402
436
  function setOpened(next,options={}){
437
+ if(destroyed){
438
+ return false;
439
+ }
403
440
  const changed=next!==opened;
404
441
 
405
442
  if(next&&changed){
@@ -415,16 +452,7 @@
415
452
  return false;
416
453
  }
417
454
 
418
- host.dispatchEvent(
419
- new CustomEvent(
420
- next?'assistant-opened':'assistant-closed',
421
- {
422
- detail:{assistant:host,layout:host.layout},
423
- bubbles:true,
424
- composed:true
425
- }
426
- )
427
- );
455
+ emit(next?'assistant-opened':'assistant-closed',{assistant:host,layout:host.layout},{opened:next,layout:host.layout});
428
456
 
429
457
  if(next&&options.focus!==false){
430
458
  requestAnimationFrame(()=>focusComposer());
@@ -503,6 +531,9 @@
503
531
  }
504
532
 
505
533
  function setState(next='ready',message=''){
534
+ if(destroyed){
535
+ return shell.dataset.state;
536
+ }
506
537
  if(next==='auto'||next===null){
507
538
  host.removeAttribute('data-state');
508
539
  host.removeAttribute('data-state-message');
@@ -523,16 +554,25 @@
523
554
  function syncComposerState(){
524
555
  const disabled=shell.dataset.state==='pending';
525
556
  const controls=composerControls();
557
+ const currentControls=new Set(controls);
558
+ for(const [control,original] of pendingDisabledState){
559
+ if(!disabled||!currentControls.has(control)){
560
+ restoreComposerControl(control,original);
561
+ pendingDisabledState.delete(control);
562
+ }
563
+ }
526
564
  controls.forEach(control=>{
527
565
  if(control.matches('button,input,textarea,select')){
528
566
  if(disabled&&!pendingDisabledState.has(control)){
529
- pendingDisabledState.set(control,control.disabled);
567
+ pendingDisabledState.set(
568
+ control,
569
+ {
570
+ disabled:control.disabled,
571
+ ariaDisabled:control.getAttribute('aria-disabled')
572
+ }
573
+ );
530
574
  control.disabled=true;
531
575
  control.setAttribute('aria-disabled','true');
532
- }else if(!disabled&&pendingDisabledState.has(control)){
533
- control.disabled=pendingDisabledState.get(control);
534
- control.toggleAttribute('aria-disabled',control.disabled);
535
- pendingDisabledState.delete(control);
536
576
  }
537
577
  }
538
578
  });
@@ -551,7 +591,26 @@
551
591
  return controls;
552
592
  }
553
593
 
594
+ function restoreComposerControl(control,original){
595
+ control.disabled=original.disabled;
596
+ if(original.ariaDisabled===null){
597
+ control.removeAttribute('aria-disabled');
598
+ }else{
599
+ control.setAttribute('aria-disabled',original.ariaDisabled);
600
+ }
601
+ }
602
+
603
+ function restoreComposerControls(){
604
+ for(const [control,original] of pendingDisabledState){
605
+ restoreComposerControl(control,original);
606
+ }
607
+ pendingDisabledState.clear();
608
+ }
609
+
554
610
  function focusComposer(){
611
+ if(destroyed){
612
+ return null;
613
+ }
555
614
  const controls=composerControls();
556
615
  const target=controls.find(
557
616
  element=>element.matches?.('textarea,input:not([type="hidden"]),[contenteditable="true"]')&&!element.disabled
@@ -562,6 +621,9 @@
562
621
  }
563
622
 
564
623
  function scrollToEnd(options={}){
624
+ if(destroyed){
625
+ return 0;
626
+ }
565
627
  const messages=root.querySelector('.messages');
566
628
  messages.scrollTo({
567
629
  top:messages.scrollHeight,
@@ -595,37 +657,23 @@
595
657
  }
596
658
 
597
659
  function send(message='',context={}){
660
+ if(destroyed){
661
+ return false;
662
+ }
598
663
  const normalized=String(message??'').trim();
599
664
 
600
665
  if(!normalized){
601
666
  return false;
602
667
  }
603
668
 
604
- return host.dispatchEvent(
605
- new CustomEvent(
606
- 'assistant-send',
607
- {
608
- detail:{...context,message:normalized,assistant:host},
609
- bubbles:true,
610
- composed:true,
611
- cancelable:true
612
- }
613
- )
614
- );
669
+ return emit('assistant-send',{...compatibilityDetail(context),message:normalized,assistant:host},{messageLength:normalized.length,layout:host.layout},{cancelable:true,operationId:nextOperationId('send')});
615
670
  }
616
671
 
617
672
  function clear(detail={}){
618
- return host.dispatchEvent(
619
- new CustomEvent(
620
- 'assistant-clear',
621
- {
622
- detail:{...detail,assistant:host},
623
- bubbles:true,
624
- composed:true,
625
- cancelable:true
626
- }
627
- )
628
- );
673
+ if(destroyed){
674
+ return false;
675
+ }
676
+ return emit('assistant-clear',{...compatibilityDetail(detail),assistant:host},{layout:host.layout},{cancelable:true,operationId:nextOperationId('clear')});
629
677
  }
630
678
 
631
679
  function onHostAction(event){
@@ -695,6 +743,20 @@
695
743
  }
696
744
  }
697
745
 
746
+ function destroy(){
747
+ if(destroyed){
748
+ return false;
749
+ }
750
+ restoreComposerControls();
751
+ destroyed=true;
752
+ lifecycleController.abort();
753
+ observer.disconnect();
754
+ returnFocus();
755
+ events.dispose();
756
+ host.ready=false;
757
+ return true;
758
+ }
759
+
698
760
  syncLabels();
699
761
  syncLayout();
700
762
  syncOpenedState();
@@ -702,14 +764,5 @@
702
764
  syncComposerState();
703
765
 
704
766
  host.ready=true;
705
- host.dispatchEvent(
706
- new CustomEvent(
707
- 'assistant-ready',
708
- {
709
- detail:{assistant:host},
710
- bubbles:true,
711
- composed:true
712
- }
713
- )
714
- );
767
+ emit('assistant-ready',{assistant:host},{ready:true});
715
768
  </script>
@@ -1,8 +1,11 @@
1
1
  <link rel="stylesheet" href="./arcane/css/theme.css?v=1"><link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
2
2
  <style>:host{display:block;max-width:34rem}.calculator{display:grid;gap:var(--arcane-space-3);padding:var(--arcane-space-4)}.display{display:grid;gap:var(--arcane-space-2)}.display input{font:600 1.3rem/1.4 ui-monospace,monospace;text-align:right}.result{min-height:2.5rem;margin:0;text-align:right;font-size:1.8rem;font-weight:750;overflow-wrap:anywhere}.keys{display:grid;gap:var(--arcane-space-2);grid-template-columns:repeat(5,1fr)}.keys button{min-width:0;padding:.7rem .35rem}.status{min-height:1.4rem;margin:0;color:var(--arcane-muted)}@media(max-width:28rem){.keys{grid-template-columns:repeat(4,1fr)}}</style>
3
3
  <section class="calculator arcane-card"><div class="display arcane-field"><label for="expression">Expression</label><input id="expression" inputmode="decimal" autocomplete="off" spellcheck="false" value=""><p id="result" class="result" aria-live="polite">0</p></div><div id="keys" class="keys" aria-label="Calculator keypad"></div><p id="status" class="status" role="status"></p></section>
4
- <script type="module">const host=this,root=host.shadowRoot,input=root.querySelector('#expression'),result=root.querySelector('#result'),status=root.querySelector('#status'),keys=root.querySelector('#keys');const componentURL=new URL(host.getAttribute('href')||'./arcane/components/calculator.html',document.baseURI);const {default:CalculatorEngine}=await import(new URL('../modules/CalculatorEngine.js',componentURL));const engine=new CalculatorEngine(),definitions=['7','8','9','/','sqrt(','4','5','6','*','^','1','2','3','-','%','0','.','(',')','+','pi','e','C','⌫','='];
4
+ <script type="module">const host=this,root=host.shadowRoot,input=root.querySelector('#expression'),result=root.querySelector('#result'),status=root.querySelector('#status'),keys=root.querySelector('#keys');const componentURL=new URL(host.getAttribute('href')||'./arcane/components/calculator.html',document.baseURI);const {default:CalculatorEngine}=await import(new URL('../modules/CalculatorEngine.js',componentURL));const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.calculator',eventTypes:['calculation-complete','calculation-error','calculator-ready']}),lifecycleController=new AbortController();const engine=new CalculatorEngine(),definitions=['7','8','9','/','sqrt(','4','5','6','*','^','1','2','3','-','%','0','.','(',')','+','pi','e','C','⌫','='];let destroyed=false,operationSequence=0;
5
5
  for(const value of definitions){const button=document.createElement('button');button.type='button';button.className=value==='='?'arcane-button':'arcane-button arcane-button--secondary';button.textContent=value;button.dataset.value=value;keys.append(button)}
6
- keys.addEventListener('click',event=>{const value=event.target.closest('[data-value]')?.dataset.value;if(!value)return;if(value==='C'){input.value='';result.textContent='0';status.textContent='';return}if(value==='⌫'){input.value=input.value.slice(0,-1);return}if(value==='='){calculate();return}input.value+=value;input.focus()});input.addEventListener('keydown',event=>{if(event.key==='Enter'){event.preventDefault();calculate()}});
7
- function calculate(){try{const calculation=engine.calculate(input.value);result.textContent=new Intl.NumberFormat(undefined,{maximumSignificantDigits:14}).format(calculation.result);status.textContent='';host.dispatchEvent(new CustomEvent('calculation-complete',{bubbles:true,composed:true,detail:{calculation}}))}catch(error){status.textContent=error.message;host.dispatchEvent(new CustomEvent('calculation-error',{bubbles:true,composed:true,detail:{error,expression:input.value}}))}}
8
- host.calculate=calculate;host.ready=true;host.dispatchEvent(new CustomEvent('calculator-ready',{bubbles:true,composed:true}));</script>
6
+ keys.addEventListener('click',event=>{const value=event.target.closest('[data-value]')?.dataset.value;if(!value)return;if(value==='C'){input.value='';result.textContent='0';status.textContent='';return}if(value==='⌫'){input.value=input.value.slice(0,-1);return}if(value==='='){calculate();return}input.value+=value;input.focus()},{signal:lifecycleController.signal});input.addEventListener('keydown',event=>{if(event.key==='Enter'){event.preventDefault();calculate()}},{signal:lifecycleController.signal});
7
+ function nextOperationId(){operationSequence+=1;return `${events.descriptor.instanceId}:calculate:${operationSequence}`}
8
+ function emit(type,detail={},publicDetail={},operationId=null){if(destroyed)return false;const publication=events.dispatch(type,detail,{publicDetail,operationId});return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true})}
9
+ function calculate(){if(destroyed)return false;const operationId=nextOperationId();try{const calculation=engine.calculate(input.value);result.textContent=new Intl.NumberFormat(undefined,{maximumSignificantDigits:14}).format(calculation.result);status.textContent='';emit('calculation-complete',{calculation},{succeeded:true},operationId)}catch(error){const code=String(error?.code||'ARCANE_CALCULATOR_EXPRESSION_EVALUATION_FAILED'),reason='expression-evaluation-failed';status.textContent=error.message;emit('calculation-error',{error,expression:input.value,code,reason},{code,reason,expressionLength:input.value.length},operationId)}}
10
+ function destroy(){if(destroyed)return false;destroyed=true;lifecycleController.abort('calculator-destroyed');engine.destroy();events.dispose();host.ready=false;return true}
11
+ host.calculate=calculate;host.destroy=destroy;host.ready=true;emit('calculator-ready',{}, {ready:true});</script>
@@ -172,6 +172,9 @@
172
172
  normalizeChartOptions,
173
173
  normalizeChartRows
174
174
  }=await import(moduleURL('ComponentContracts.js'));
175
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
176
+ const events=createArcaneEventSource(host,{source:'arcane.component.chart',eventTypes:['chart-remove','chart-ready']});
177
+ const lifecycleController=new AbortController();
175
178
  const title=host.shadowRoot.querySelector('#chartTitle');
176
179
  const info=host.shadowRoot.querySelector('#chartInfo');
177
180
  const plot=host.shadowRoot.querySelector('#plot');
@@ -190,6 +193,8 @@
190
193
  let selectedIndex=-1;
191
194
  let resizeFrame=0;
192
195
  let renderGeneration=0;
196
+ let operationSequence=0;
197
+ let destroyed=false;
193
198
  let options=normalizeChartOptions(optionsFromDataset());
194
199
 
195
200
  host.addData=addData;
@@ -203,18 +208,22 @@
203
208
  host,
204
209
  {
205
210
  chartStyle:{
211
+ configurable:true,
206
212
  get:()=>options.style,
207
213
  set:value=>void configure({style:value})
208
214
  },
209
215
  chartTitle:{
216
+ configurable:true,
210
217
  get:()=>options.title,
211
218
  set:value=>void configure({title:value})
212
219
  },
213
220
  dataSet:{
221
+ configurable:true,
214
222
  get:()=>rows.map(row=>row.slice()),
215
223
  set:value=>void update(value)
216
224
  },
217
225
  options:{
226
+ configurable:true,
218
227
  get:getOptions,
219
228
  set:value=>void configure(value)
220
229
  }
@@ -223,21 +232,22 @@
223
232
 
224
233
  removeButton.addEventListener(
225
234
  'click',
226
- ()=>host.dispatchEvent(
227
- new CustomEvent(
228
- 'chart-remove',
229
- {
230
- bubbles:true,
231
- composed:true,
232
- detail:{key:options.key}
233
- }
234
- )
235
- )
235
+ ()=>{
236
+ const operationId=`${events.descriptor.instanceId}:remove:${++operationSequence}`;
237
+ const publication=events.dispatch('chart-remove',{key:options.key},{operationId,publicDetail:{key:options.key}});
238
+ projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
239
+ },
240
+ {signal:lifecycleController.signal}
241
+ );
242
+ plot.addEventListener(
243
+ 'keydown',
244
+ navigatePoints,
245
+ {signal:lifecycleController.signal}
236
246
  );
237
- plot.addEventListener('keydown',navigatePoints);
238
247
  plot.addEventListener(
239
248
  'pointerleave',
240
- ()=>updateSelectedValue(selectedIndex,'selected')
249
+ ()=>updateSelectedValue(selectedIndex,'selected'),
250
+ {signal:lifecycleController.signal}
241
251
  );
242
252
 
243
253
  const resizeObserver=typeof ResizeObserver==='function'
@@ -286,6 +296,9 @@
286
296
  }
287
297
 
288
298
  async function configure(input={}){
299
+ if(destroyed){
300
+ return false;
301
+ }
289
302
  const suppliedData=Object.prototype.hasOwnProperty.call(input||{},'data')
290
303
  ||Object.prototype.hasOwnProperty.call(input||{},'dataset')
291
304
  ||Object.prototype.hasOwnProperty.call(input||{},'datasets');
@@ -302,18 +315,27 @@
302
315
  }
303
316
 
304
317
  async function populate(data=[],input={}){
318
+ if(destroyed){
319
+ return [];
320
+ }
305
321
  options=normalizeChartOptions(input,options);
306
322
  renderOptions();
307
323
  return update(data);
308
324
  }
309
325
 
310
326
  async function update(data=[]){
327
+ if(destroyed){
328
+ return [];
329
+ }
311
330
  rows=normalizeChartRows(data,options);
312
331
  options={...options,data};
313
332
  return renderRows();
314
333
  }
315
334
 
316
335
  async function renderRows(){
336
+ if(destroyed){
337
+ return [];
338
+ }
317
339
  const generation=++renderGeneration;
318
340
  selectedIndex=rows.length-1;
319
341
 
@@ -329,11 +351,20 @@
329
351
  plot.classList.remove('hidden');
330
352
  chartValue.classList.remove('hidden');
331
353
  empty.classList.add('hidden');
332
- uPlot=uPlot||await loadChartLibrary();
354
+ let library;
355
+ try{
356
+ library=uPlot||await loadChartLibrary();
357
+ }catch(error){
358
+ if(destroyed||generation!==renderGeneration){
359
+ return host.dataSet;
360
+ }
361
+ throw error;
362
+ }
333
363
 
334
- if(generation!==renderGeneration){
364
+ if(destroyed||generation!==renderGeneration){
335
365
  return host.dataSet;
336
366
  }
367
+ uPlot=library;
337
368
  if(chart){
338
369
  chart.setData(toChartData(rows));
339
370
  updateSelectedValue(selectedIndex,'latest');
@@ -344,6 +375,9 @@
344
375
  }
345
376
 
346
377
  async function addData(data=[]){
378
+ if(destroyed){
379
+ return [];
380
+ }
347
381
  const additions=normalizeChartRows(data,options);
348
382
  const merged=new Map(rows.concat(additions).map(row=>[row[0],row]));
349
383
  rows=[...merged.values()].sort((left,right)=>left[0]-right[0]);
@@ -633,6 +667,11 @@
633
667
  }
634
668
 
635
669
  function destroy(){
670
+ if(destroyed){
671
+ return false;
672
+ }
673
+ destroyed=true;
674
+ lifecycleController.abort();
636
675
  renderGeneration++;
637
676
  resizeObserver?.disconnect();
638
677
  if(!resizeObserver){
@@ -644,12 +683,14 @@
644
683
  }
645
684
  destroyChart();
646
685
  rows=[];
686
+ events.dispose();
687
+ host.ready=false;
688
+ return true;
647
689
  }
648
690
 
649
691
  renderOptions();
650
692
  updateSelectedValue(-1);
651
693
  host.ready=true;
652
- host.dispatchEvent(
653
- new CustomEvent('chart-ready',{bubbles:true,composed:true})
654
- );
694
+ const readyPublication=events.dispatch('chart-ready',{}, {publicDetail:{ready:true}});
695
+ projectArcaneDOMEvent(host,readyPublication.occurrence,{bubbles:true,composed:true});
655
696
  </script>