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
@@ -353,6 +353,9 @@
353
353
 
354
354
  const host=this;
355
355
  const {default:waitForComponent}=await import('../modules/WaitForComponent.js');
356
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
357
+ const events=createArcaneEventSource(host,{source:'arcane.component.file-manager',eventTypes:['file-manager-action','file-manager-select','file-manager-open','file-manager-ready']});
358
+ const lifecycleController=new AbortController();
356
359
 
357
360
  const fileManager=host.shadowRoot.querySelector('.file-manager');
358
361
  const componentStyles=host.shadowRoot.querySelector('style');
@@ -377,6 +380,16 @@
377
380
  let selectedPath='';
378
381
  let treeItems=new Map();
379
382
  let treeLoadSequence=0;
383
+ let gridLoadSequence=0;
384
+ let providerGeneration=0;
385
+ let filterGeneration=0;
386
+ let treeOpenGeneration=0;
387
+ let directoryOpenGeneration=0;
388
+ let fileOpenGeneration=0;
389
+ let deleteGeneration=0;
390
+ let selectionGeneration=0;
391
+ let operationSequence=0;
392
+ let destroyed=false;
380
393
 
381
394
  host.ready=false;
382
395
  host.loadAll=loadAll;
@@ -384,6 +397,7 @@
384
397
  host.setFilter=setFilter;
385
398
  host.select=select;
386
399
  host.clearSelection=clearSelection;
400
+ host.destroy=destroy;
387
401
 
388
402
  Object.defineProperty(
389
403
  host,
@@ -440,7 +454,7 @@
440
454
  window.addEventListener(
441
455
  'dbopfs-ready',
442
456
  init,
443
- {once:true}
457
+ {once:true,signal:lifecycleController.signal}
444
458
  );
445
459
 
446
460
  if(window.dbopfs?.ready){
@@ -450,8 +464,13 @@
450
464
  fileUpload.addEventListener(
451
465
  'change',
452
466
  ()=>uploadFiles().catch(
453
- error=>console.error('Unable to upload files:',error)
454
- )
467
+ error=>{
468
+ if(!destroyed){
469
+ console.error('Unable to upload files:',error);
470
+ }
471
+ }
472
+ ),
473
+ {signal:lifecycleController.signal}
455
474
  );
456
475
 
457
476
  fileManager.addEventListener(
@@ -465,7 +484,11 @@
465
484
 
466
485
  if(layout==='grid'){
467
486
  openDirectory(directoryElement.dataset.directory).catch(
468
- error=>console.error('Unable to open directory:',error)
487
+ error=>{
488
+ if(!destroyed){
489
+ console.error('Unable to open directory:',error);
490
+ }
491
+ }
469
492
  );
470
493
  return;
471
494
  }
@@ -474,26 +497,53 @@
474
497
  return;
475
498
  }
476
499
 
477
- }
500
+ },
501
+ {signal:lifecycleController.signal}
478
502
  );
479
503
 
480
- fileManager.addEventListener('keydown',onTreeKeydown);
504
+ fileManager.addEventListener(
505
+ 'keydown',
506
+ onTreeKeydown,
507
+ {signal:lifecycleController.signal}
508
+ );
481
509
 
482
- function emit(name,detail={},cancelable=false){
483
- return host.dispatchEvent(
484
- new CustomEvent(
485
- name,
486
- {
487
- bubbles:true,
488
- cancelable:cancelable,
489
- composed:true,
490
- detail:detail
491
- }
492
- )
493
- );
510
+ function nextOperationId(action='operation'){
511
+ operationSequence+=1;
512
+ return `${events.descriptor.instanceId}:${action}:${operationSequence}`;
494
513
  }
495
514
 
496
- function emitAction(action,entry={},detail={},cancelable=false){
515
+ function publicEventDetail(name,detail={}){
516
+ if(name==='file-manager-action'){
517
+ const publicDetail={action:String(detail.action||''),kind:String(detail.entry?.kind||''),hasEntry:Boolean(detail.path)};
518
+ if(detail.code){
519
+ publicDetail.code=String(detail.code);
520
+ }
521
+ if(detail.reason){
522
+ publicDetail.reason=String(detail.reason);
523
+ }
524
+ return publicDetail;
525
+ }
526
+ if(name==='file-manager-open'){
527
+ return {trigger:String(detail.trigger||''),kind:String(detail.entry?.kind||''),hasEntry:Boolean(detail.path)};
528
+ }
529
+ if(name==='file-manager-select'){
530
+ return {selected:Boolean(detail.path),kind:String(detail.entry?.kind||'')};
531
+ }
532
+ return {};
533
+ }
534
+
535
+ function emit(name,detail={},cancelable=false,operationId=null){
536
+ if(destroyed){
537
+ return false;
538
+ }
539
+ const publication=events.dispatch(name,detail,{publicDetail:publicEventDetail(name,detail),cancelable,operationId});
540
+ if(cancelable&&!publication.accepted){
541
+ return false;
542
+ }
543
+ return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,cancelable,composed:true});
544
+ }
545
+
546
+ function emitAction(action,entry={},detail={},cancelable=false,operationId=null){
497
547
  return emit(
498
548
  'file-manager-action',
499
549
  {
@@ -502,15 +552,20 @@
502
552
  entry:entry,
503
553
  ...detail
504
554
  },
505
- cancelable
555
+ cancelable,
556
+ operationId
506
557
  );
507
558
  }
508
559
 
509
560
  async function setProvider(nextProvider=null){
561
+ if(destroyed){
562
+ return null;
563
+ }
510
564
  if(nextProvider!==null&&typeof nextProvider?.list!=='function'){
511
565
  throw new TypeError('File manager providers must expose list(path).');
512
566
  }
513
567
 
568
+ const generation=++providerGeneration;
514
569
  provider=nextProvider;
515
570
 
516
571
  if(layout==='tree'){
@@ -521,21 +576,25 @@
521
576
  }
522
577
  }
523
578
 
524
- return provider;
579
+ return destroyed||generation!==providerGeneration?null:provider;
525
580
  }
526
581
 
527
582
  async function setFilter(nextFilter=null){
583
+ if(destroyed){
584
+ return null;
585
+ }
528
586
  if(nextFilter!==null&&typeof nextFilter!=='function'){
529
587
  throw new TypeError('File manager filters must be functions or null.');
530
588
  }
531
589
 
590
+ const generation=++filterGeneration;
532
591
  filter=nextFilter;
533
592
 
534
593
  if(initialized){
535
594
  await loadAll();
536
595
  }
537
596
 
538
- return filter;
597
+ return destroyed||generation!==filterGeneration?null:filter;
539
598
  }
540
599
 
541
600
  function normalizeProviderPath(path=''){
@@ -709,13 +768,17 @@
709
768
  }
710
769
 
711
770
  if(restoredPath){
712
- const restored=await select(
771
+ const restoration=select(
713
772
  restoredPath,
714
773
  {emit:false,focus:false}
715
774
  );
775
+ const restorationGeneration=selectionGeneration;
776
+ const restored=await restoration;
716
777
 
717
- if(!restored){
718
- selectedPath='';
778
+ if(!restored
779
+ &&selectionGeneration===restorationGeneration
780
+ &&selectedPath===restoredPath){
781
+ clearSelection({emit:false});
719
782
  }
720
783
  }
721
784
 
@@ -729,7 +792,17 @@
729
792
  message.className='tree-error';
730
793
  message.innerText='Unable to list files.';
731
794
  fileManager.append(message);
732
- emitAction('error',{}, {error:error});
795
+ emitAction(
796
+ 'error',
797
+ {},
798
+ {
799
+ error,
800
+ code:String(error?.code||'ARCANE_FILE_MANAGER_ROOT_LIST_FAILED'),
801
+ reason:'file-root-list-failed'
802
+ },
803
+ false,
804
+ nextOperationId('root-list-error')
805
+ );
733
806
  throw error;
734
807
  }finally{
735
808
  if(sequence===treeLoadSequence){
@@ -800,20 +873,35 @@
800
873
  item.dataset.fileCount='0';
801
874
  }
802
875
 
803
- row.addEventListener('click',async()=>{
804
- await select(entry.path,{entry:entry,focus:true});
876
+ row.addEventListener(
877
+ 'click',
878
+ async()=>{
879
+ const selected=await select(entry.path,{entry:entry,focus:true});
880
+ if(!selected||destroyed){
881
+ return;
882
+ }
805
883
 
806
- if(entry.kind==='directory'){
807
- await toggleTreeItem(item);
808
- }
809
- });
810
- row.addEventListener('dblclick',async event=>{
811
- if(entry.kind==='file'){
812
- event.preventDefault();
813
- await openTreeItem(item,'double-click');
814
- }
815
- });
816
- item.addEventListener('focus',()=>setRovingTreeItem(item,false));
884
+ if(entry.kind==='directory'){
885
+ await toggleTreeItem(item);
886
+ }
887
+ },
888
+ {signal:lifecycleController.signal}
889
+ );
890
+ row.addEventListener(
891
+ 'dblclick',
892
+ async event=>{
893
+ if(entry.kind==='file'){
894
+ event.preventDefault();
895
+ await openTreeItem(item,'double-click');
896
+ }
897
+ },
898
+ {signal:lifecycleController.signal}
899
+ );
900
+ item.addEventListener(
901
+ 'focus',
902
+ ()=>setRovingTreeItem(item,false),
903
+ {signal:lifecycleController.signal}
904
+ );
817
905
 
818
906
  return item;
819
907
  }
@@ -857,7 +945,7 @@
857
945
  }
858
946
 
859
947
  async function expandTreeItem(item,{emitEvent=true}={}){
860
- if(!item||item.dataset.kind!=='directory'){
948
+ if(destroyed||!item||item.dataset.kind!=='directory'){
861
949
  return false;
862
950
  }
863
951
 
@@ -896,16 +984,33 @@
896
984
  item.dataset.loaded='true';
897
985
  updateTreeCounts(item);
898
986
  }catch(error){
987
+ if(destroyed||sequence!==treeLoadSequence||!fileManager.contains(item)){
988
+ return false;
989
+ }
899
990
  group.replaceChildren();
900
991
  const message=document.createElement('li');
901
992
  message.className='tree-error';
902
993
  message.setAttribute('role','none');
903
994
  message.innerText='Unable to list this folder.';
904
995
  group.append(message);
905
- emitAction('error',entry,{error:error});
996
+ emitAction(
997
+ 'error',
998
+ entry,
999
+ {
1000
+ error,
1001
+ code:String(error?.code||'ARCANE_FILE_MANAGER_DIRECTORY_LIST_FAILED'),
1002
+ reason:'directory-list-failed'
1003
+ },
1004
+ false,
1005
+ nextOperationId('directory-list-error')
1006
+ );
906
1007
  return false;
907
1008
  }finally{
908
- item.removeAttribute('aria-busy');
1009
+ if(!destroyed
1010
+ &&sequence===treeLoadSequence
1011
+ &&fileManager.contains(item)){
1012
+ item.removeAttribute('aria-busy');
1013
+ }
909
1014
  }
910
1015
  }
911
1016
 
@@ -920,6 +1025,9 @@
920
1025
  }
921
1026
 
922
1027
  function collapseTreeItem(item,{emitEvent=true}={}){
1028
+ if(destroyed){
1029
+ return false;
1030
+ }
923
1031
  const group=item?.querySelector(':scope>.tree-group');
924
1032
 
925
1033
  if(!group){
@@ -937,6 +1045,9 @@
937
1045
  }
938
1046
 
939
1047
  async function toggleTreeItem(item){
1048
+ if(destroyed){
1049
+ return false;
1050
+ }
940
1051
  if(item?.getAttribute('aria-expanded')==='true'){
941
1052
  return collapseTreeItem(item);
942
1053
  }
@@ -994,6 +1105,10 @@
994
1105
  }
995
1106
 
996
1107
  async function select(path='',options={}){
1108
+ if(destroyed){
1109
+ return false;
1110
+ }
1111
+ const generation=++selectionGeneration;
997
1112
  const normalizedPath=normalizeProviderPath(path);
998
1113
 
999
1114
  if(!normalizedPath){
@@ -1006,6 +1121,10 @@
1006
1121
  if(layout==='tree'){
1007
1122
  item=await revealTreePath(normalizedPath);
1008
1123
 
1124
+ if(destroyed||generation!==selectionGeneration){
1125
+ return false;
1126
+ }
1127
+
1009
1128
  if(!item){
1010
1129
  return false;
1011
1130
  }
@@ -1040,6 +1159,10 @@
1040
1159
  }
1041
1160
 
1042
1161
  function clearSelection(options={}){
1162
+ if(destroyed){
1163
+ return false;
1164
+ }
1165
+ selectionGeneration+=1;
1043
1166
  const changed=Boolean(selectedPath);
1044
1167
  selectedPath='';
1045
1168
 
@@ -1061,16 +1184,24 @@
1061
1184
  }
1062
1185
 
1063
1186
  async function openTreeItem(item,trigger='open'){
1187
+ if(destroyed){
1188
+ return false;
1189
+ }
1190
+ const generation=++treeOpenGeneration;
1064
1191
  const entry=item?._fileManagerEntry;
1065
1192
 
1066
1193
  if(!entry||entry.kind!=='file'){
1067
1194
  return false;
1068
1195
  }
1069
1196
 
1070
- await select(entry.path,{entry:entry,focus:true});
1197
+ const selected=await select(entry.path,{entry:entry,focus:true});
1198
+ if(!selected||destroyed||generation!==treeOpenGeneration){
1199
+ return false;
1200
+ }
1071
1201
  const detail={path:entry.path,entry:entry,trigger:trigger};
1072
- const openAllowed=emit('file-manager-open',detail,true);
1073
- const actionAllowed=emitAction('open',entry,{trigger:trigger},true);
1202
+ const operationId=nextOperationId('open');
1203
+ const openAllowed=emit('file-manager-open',detail,true,operationId);
1204
+ const actionAllowed=emitAction('open',entry,{trigger:trigger},true,operationId);
1074
1205
 
1075
1206
  if(!openAllowed||!actionAllowed||openMode==='event'){
1076
1207
  return true;
@@ -1079,9 +1210,24 @@
1079
1210
  if(typeof activeTreeProvider().open==='function'){
1080
1211
  try{
1081
1212
  await activeTreeProvider().open(entry);
1213
+ if(destroyed||generation!==treeOpenGeneration){
1214
+ return false;
1215
+ }
1082
1216
  }catch(error){
1083
- emitAction('error',entry,{error:error});
1084
- console.error('Unable to open file:',error);
1217
+ if(!destroyed&&generation===treeOpenGeneration){
1218
+ emitAction(
1219
+ 'error',
1220
+ entry,
1221
+ {
1222
+ error,
1223
+ code:String(error?.code||'ARCANE_FILE_MANAGER_ENTRY_OPEN_FAILED'),
1224
+ reason:'file-open-failed'
1225
+ },
1226
+ false,
1227
+ nextOperationId('open-error')
1228
+ );
1229
+ console.error('Unable to open file:',error);
1230
+ }
1085
1231
  return false;
1086
1232
  }
1087
1233
  }
@@ -1169,18 +1315,34 @@
1169
1315
  }
1170
1316
 
1171
1317
  async function openDirectory(directory=''){
1318
+ if(destroyed){
1319
+ return false;
1320
+ }
1321
+ const generation=++directoryOpenGeneration;
1172
1322
  const modal=await waitForComponent(
1173
1323
  directoryModal,
1174
1324
  {
1175
1325
  methods:['populate','open'],
1176
1326
  property:'ready',
1177
- event:'modal-ready'
1178
- }
1327
+ event:'modal-ready',
1328
+ signal:lifecycleController.signal
1329
+ },
1330
+ {signal:lifecycleController.signal}
1179
1331
  );
1180
- const files=(await dbopfs.getAllKeys(directory))
1332
+ if(destroyed||generation!==directoryOpenGeneration){
1333
+ return false;
1334
+ }
1335
+ const keys=await dbopfs.getAllKeys(directory);
1336
+ if(destroyed||generation!==directoryOpenGeneration){
1337
+ return false;
1338
+ }
1339
+ const files=keys
1181
1340
  .filter(isVisibleFileName)
1182
1341
  .sort();
1183
1342
  const metadata=await getFilesMetadata(directory,files);
1343
+ if(destroyed||generation!==directoryOpenGeneration){
1344
+ return false;
1345
+ }
1184
1346
  const content=document.createElement('section');
1185
1347
  const styles=componentStyles.cloneNode(true);
1186
1348
  const heading=document.createElement('h1');
@@ -1195,7 +1357,8 @@
1195
1357
  uploadButton.innerText='Upload Files';
1196
1358
  uploadButton.addEventListener(
1197
1359
  'click',
1198
- ()=>selectFiles(directory)
1360
+ ()=>selectFiles(directory),
1361
+ {signal:lifecycleController.signal}
1199
1362
  );
1200
1363
  list.className='directory-file-list';
1201
1364
  list.dataset.layout='files';
@@ -1222,18 +1385,31 @@
1222
1385
  }
1223
1386
 
1224
1387
  await modal.populate(content,false);
1388
+ if(destroyed||generation!==directoryOpenGeneration){
1389
+ return false;
1390
+ }
1225
1391
  modal.open();
1392
+ return true;
1226
1393
  }
1227
1394
 
1228
1395
  async function openFile(directory='',fileName=''){
1396
+ if(destroyed){
1397
+ return false;
1398
+ }
1399
+ const generation=++fileOpenGeneration;
1229
1400
  const modal=await waitForComponent(
1230
1401
  fileModal,
1231
1402
  {
1232
1403
  methods:['populate','open'],
1233
1404
  property:'ready',
1234
- event:'modal-ready'
1235
- }
1405
+ event:'modal-ready',
1406
+ signal:lifecycleController.signal
1407
+ },
1408
+ {signal:lifecycleController.signal}
1236
1409
  );
1410
+ if(destroyed||generation!==fileOpenGeneration){
1411
+ return false;
1412
+ }
1237
1413
  const content=document.createElement('section');
1238
1414
  const styles=componentStyles.cloneNode(true);
1239
1415
  const heading=document.createElement('h1');
@@ -1254,8 +1430,13 @@
1254
1430
  deleteButton.addEventListener(
1255
1431
  'click',
1256
1432
  ()=>confirmDelete(directory,fileName,true).catch(
1257
- error=>console.error('Unable to delete file:',error)
1258
- )
1433
+ error=>{
1434
+ if(!destroyed){
1435
+ console.error('Unable to delete file:',error);
1436
+ }
1437
+ }
1438
+ ),
1439
+ {signal:lifecycleController.signal}
1259
1440
  );
1260
1441
  status.innerText='Opening file...';
1261
1442
  actions.append(deleteButton);
@@ -1267,13 +1448,22 @@
1267
1448
  status
1268
1449
  );
1269
1450
 
1270
- modal.populate(content,false);
1451
+ await modal.populate(content,false);
1452
+ if(destroyed||generation!==fileOpenGeneration){
1453
+ return false;
1454
+ }
1271
1455
  modal.open();
1272
1456
 
1273
1457
  try{
1274
1458
  const {default:FileEntity}=await fileEntityPromise;
1459
+ if(destroyed||generation!==fileOpenGeneration){
1460
+ return false;
1461
+ }
1275
1462
  const file=new FileEntity(fileName,directory);
1276
1463
  const fileRef=await file.open();
1464
+ if(destroyed||generation!==fileOpenGeneration){
1465
+ return false;
1466
+ }
1277
1467
 
1278
1468
  status.remove();
1279
1469
 
@@ -1282,7 +1472,13 @@
1282
1472
 
1283
1473
  if(!layout){
1284
1474
  await dbopfs.delete(directory,fileName);
1475
+ if(destroyed||generation!==fileOpenGeneration){
1476
+ return false;
1477
+ }
1285
1478
  await loadAll();
1479
+ if(destroyed||generation!==fileOpenGeneration){
1480
+ return false;
1481
+ }
1286
1482
  empty.innerText='File was empty and has been deleted';
1287
1483
  }else{
1288
1484
  empty.innerText='File is empty';
@@ -1290,11 +1486,19 @@
1290
1486
 
1291
1487
  content.append(empty);
1292
1488
  }else{
1293
- content.append(await renderFile(fileRef,{directory,fileName}));
1489
+ const rendered=await renderFile(fileRef,{directory,fileName});
1490
+ if(destroyed||generation!==fileOpenGeneration){
1491
+ return false;
1492
+ }
1493
+ content.append(rendered);
1294
1494
  }
1495
+ return true;
1295
1496
  }catch(error){
1296
- status.innerText='Unable to open this file.';
1297
- console.error('Unable to open file:',error);
1497
+ if(!destroyed&&generation===fileOpenGeneration){
1498
+ status.innerText='Unable to open this file.';
1499
+ console.error('Unable to open file:',error);
1500
+ }
1501
+ return false;
1298
1502
  }
1299
1503
  }
1300
1504
 
@@ -1479,8 +1683,9 @@
1479
1683
  async()=>{
1480
1684
  await select(entry.path,{entry:entry});
1481
1685
  const detail={path:entry.path,entry:entry,trigger:'click'};
1482
- const openAllowed=emit('file-manager-open',detail,true);
1483
- const actionAllowed=emitAction('open',entry,{trigger:'click'},true);
1686
+ const operationId=nextOperationId('open');
1687
+ const openAllowed=emit('file-manager-open',detail,true,operationId);
1688
+ const actionAllowed=emitAction('open',entry,{trigger:'click'},true,operationId);
1484
1689
 
1485
1690
  if(!openAllowed||!actionAllowed||openMode==='event'){
1486
1691
  return;
@@ -1489,19 +1694,21 @@
1489
1694
  openFile(directory,fileName).catch(
1490
1695
  error=>console.error('Unable to open file:',error)
1491
1696
  );
1492
- }
1697
+ },
1698
+ {signal:lifecycleController.signal}
1493
1699
  );
1494
1700
  deleteButton.addEventListener(
1495
1701
  'click',
1496
1702
  ()=>{
1497
- if(!emitAction('delete',entry,{},true)){
1703
+ if(!emitAction('delete',entry,{},true,nextOperationId('delete'))){
1498
1704
  return;
1499
1705
  }
1500
1706
 
1501
1707
  confirmDelete(directory,fileName).catch(
1502
1708
  error=>console.error('Unable to delete file:',error)
1503
1709
  );
1504
- }
1710
+ },
1711
+ {signal:lifecycleController.signal}
1505
1712
  );
1506
1713
 
1507
1714
  openButton.append(fileIcon,fileDetails);
@@ -1512,18 +1719,23 @@
1512
1719
  }
1513
1720
 
1514
1721
  async function confirmDelete(directory='',fileName='',closeFile=false){
1515
- if(!directory||!fileName){
1722
+ if(destroyed||!directory||!fileName){
1516
1723
  return false;
1517
1724
  }
1725
+ const generation=++deleteGeneration;
1518
1726
 
1519
1727
  const modal=await waitForComponent(
1520
1728
  deleteModal,
1521
1729
  {
1522
1730
  methods:['populate','open','close'],
1523
1731
  property:'ready',
1524
- event:'modal-ready'
1732
+ event:'modal-ready',
1733
+ signal:lifecycleController.signal
1525
1734
  }
1526
1735
  );
1736
+ if(destroyed||generation!==deleteGeneration){
1737
+ return false;
1738
+ }
1527
1739
  const content=document.createElement('section');
1528
1740
  const heading=document.createElement('h1');
1529
1741
  const message=document.createElement('p');
@@ -1539,7 +1751,8 @@
1539
1751
 
1540
1752
  cancelButton.addEventListener(
1541
1753
  'click',
1542
- ()=>modal.close(undefined,true)
1754
+ ()=>modal.close(undefined,true),
1755
+ {signal:lifecycleController.signal}
1543
1756
  );
1544
1757
  deleteButton.addEventListener(
1545
1758
  'click',
@@ -1549,24 +1762,39 @@
1549
1762
 
1550
1763
  try{
1551
1764
  await dbopfs.delete(directory,fileName);
1765
+ if(destroyed||generation!==deleteGeneration){
1766
+ return;
1767
+ }
1552
1768
  await modal.close(undefined,true);
1769
+ if(destroyed||generation!==deleteGeneration){
1770
+ return;
1771
+ }
1553
1772
 
1554
1773
  if(closeFile&&typeof fileModal.close==='function'){
1555
1774
  await fileModal.close(undefined,true);
1775
+ if(destroyed||generation!==deleteGeneration){
1776
+ return;
1777
+ }
1556
1778
  }
1557
1779
 
1558
1780
  await loadAll();
1781
+ if(destroyed||generation!==deleteGeneration){
1782
+ return;
1783
+ }
1559
1784
 
1560
1785
  if(layout==='grid'){
1561
1786
  await openDirectory(directory);
1562
1787
  }
1563
1788
  }catch(error){
1564
- message.innerText=`Unable to delete ${fileName}.`;
1565
- cancelButton.disabled=false;
1566
- deleteButton.disabled=false;
1567
- console.error('Unable to delete file:',error);
1789
+ if(!destroyed&&generation===deleteGeneration){
1790
+ message.innerText=`Unable to delete ${fileName}.`;
1791
+ cancelButton.disabled=false;
1792
+ deleteButton.disabled=false;
1793
+ console.error('Unable to delete file:',error);
1794
+ }
1568
1795
  }
1569
- }
1796
+ },
1797
+ {signal:lifecycleController.signal}
1570
1798
  );
1571
1799
 
1572
1800
  content.append(
@@ -1577,6 +1805,9 @@
1577
1805
  );
1578
1806
 
1579
1807
  await modal.populate(content,false);
1808
+ if(destroyed||generation!==deleteGeneration){
1809
+ return false;
1810
+ }
1580
1811
  modal.open();
1581
1812
 
1582
1813
  return true;
@@ -1592,6 +1823,9 @@
1592
1823
  }
1593
1824
 
1594
1825
  async function uploadFiles(){
1826
+ if(destroyed){
1827
+ return false;
1828
+ }
1595
1829
  const files=Array.from(fileUpload.files||[]);
1596
1830
  const directory=uploadDirectory;
1597
1831
  fileUpload.value='';
@@ -1601,6 +1835,9 @@
1601
1835
  }
1602
1836
 
1603
1837
  const {default:FileEntity}=await fileEntityPromise;
1838
+ if(destroyed){
1839
+ return false;
1840
+ }
1604
1841
 
1605
1842
  const results=await Promise.allSettled(
1606
1843
  files.map(
@@ -1612,8 +1849,14 @@
1612
1849
  }
1613
1850
  )
1614
1851
  );
1852
+ if(destroyed){
1853
+ return false;
1854
+ }
1615
1855
 
1616
1856
  await loadAll();
1857
+ if(destroyed){
1858
+ return false;
1859
+ }
1617
1860
 
1618
1861
  if(layout==='grid'){
1619
1862
  await openDirectory(directory);
@@ -1626,10 +1869,11 @@
1626
1869
  if(failed){
1627
1870
  throw failed.reason;
1628
1871
  }
1872
+ return true;
1629
1873
  }
1630
1874
 
1631
1875
  async function init(){
1632
- if(initialized){
1876
+ if(initialized||destroyed){
1633
1877
  return;
1634
1878
  }
1635
1879
 
@@ -1640,38 +1884,88 @@
1640
1884
  await fileEntityPromise;
1641
1885
  }
1642
1886
  await loadAll();
1887
+ if(destroyed){
1888
+ return;
1889
+ }
1643
1890
  host.ready=true;
1644
- host.dispatchEvent(
1645
- new CustomEvent('file-manager-ready')
1646
- );
1891
+ const publication=events.dispatch('file-manager-ready',{}, {publicDetail:{ready:true}});
1892
+ projectArcaneDOMEvent(host,publication.occurrence);
1647
1893
  }catch(error){
1648
- initialized=false;
1649
- console.error('Unable to initialize file manager:',error);
1894
+ if(!destroyed){
1895
+ initialized=false;
1896
+ console.error('Unable to initialize file manager:',error);
1897
+ }
1650
1898
  }
1651
1899
  }
1652
1900
 
1901
+ function destroy(){
1902
+ if(destroyed){
1903
+ return false;
1904
+ }
1905
+ destroyed=true;
1906
+ initialized=false;
1907
+ host.ready=false;
1908
+ lifecycleController.abort('file-manager-destroyed');
1909
+ treeLoadSequence+=1;
1910
+ gridLoadSequence+=1;
1911
+ providerGeneration+=1;
1912
+ filterGeneration+=1;
1913
+ treeOpenGeneration+=1;
1914
+ directoryOpenGeneration+=1;
1915
+ fileOpenGeneration+=1;
1916
+ deleteGeneration+=1;
1917
+ selectionGeneration+=1;
1918
+ if(activeFileURL){
1919
+ URL.revokeObjectURL(activeFileURL);
1920
+ activeFileURL='';
1921
+ }
1922
+ treeItems.clear();
1923
+ provider=null;
1924
+ filter=null;
1925
+ directoryModal.destroy?.();
1926
+ fileModal.destroy?.();
1927
+ deleteModal.destroy?.();
1928
+ events.dispose();
1929
+ return true;
1930
+ }
1931
+
1653
1932
  async function loadAll(){
1933
+ if(destroyed){
1934
+ return [];
1935
+ }
1936
+ selectionGeneration+=1;
1654
1937
  if(layout==='tree'){
1655
1938
  return loadTree();
1656
1939
  }
1940
+ const sequence=++gridLoadSequence;
1657
1941
 
1658
1942
  let tableNames=dirs;
1659
1943
 
1660
1944
  if(!tableNames?.length){
1661
1945
  tableNames=await dbopfs.getTableNames(Boolean(layout));
1662
1946
  }
1947
+ if(destroyed||sequence!==gridLoadSequence){
1948
+ return [];
1949
+ }
1663
1950
 
1664
1951
  tableNames.sort();
1665
1952
  fileManager.replaceChildren();
1666
1953
 
1667
1954
  for(let i=0;i<tableNames.length;i++){
1668
1955
  const tableName=tableNames[i];
1669
- const files=(await dbopfs.getAllKeys(tableName))
1956
+ const keys=await dbopfs.getAllKeys(tableName);
1957
+ if(destroyed||sequence!==gridLoadSequence){
1958
+ return [];
1959
+ }
1960
+ const files=keys
1670
1961
  .filter(isVisibleFileName)
1671
1962
  .sort();
1672
1963
  const metadata=layout==='files'
1673
1964
  ?await getFilesMetadata(tableName,files)
1674
1965
  :[];
1966
+ if(destroyed||sequence!==gridLoadSequence){
1967
+ return [];
1968
+ }
1675
1969
  const directory=document.createElement('ul');
1676
1970
  const folder=document.createElement('li');
1677
1971
  const folderIcon=document.createElement('img');
@@ -1711,5 +2005,6 @@
1711
2005
 
1712
2006
  fileManager.append(directory);
1713
2007
  }
2008
+ return tableNames;
1714
2009
  }
1715
2010
  </script>