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
@@ -4,10 +4,17 @@
4
4
  </style>
5
5
  <section class="shell"><header id="heading" class="heading" hidden><div><h2 id="title"></h2><p id="participants"></p></div><span id="badge" class="badge"></span></header><div id="messages" class="messages"><div class="blank"><div><strong>Select a conversation</strong><p>Choose a thread from the inbox, or connect a service in settings.</p></div></div></div><form id="composer" class="composer" hidden><label class="arcane-field"><span class="sr-only">Reply</span><textarea id="body" required placeholder="Write a reply"></textarea></label><div class="composer-row"><p id="status" class="status" role="status" aria-live="polite"></p><button class="arcane-button" type="submit">Send</button></div></form></section>
6
6
  <script type="module">
7
- const host=this,root=host.shadowRoot,heading=root.querySelector('#heading'),messages=root.querySelector('#messages'),composer=root.querySelector('#composer'),body=root.querySelector('#body'),status=root.querySelector('#status');let thread=null,advisories={};
8
- host.setConversation=(next,items=[],options={})=>{thread=next||null;advisories=options?.advisories instanceof Map?options.advisories:new Map();heading.hidden=!thread;composer.hidden=!thread;if(!thread){messages.innerHTML='<div class="blank"><div><strong>Select a conversation</strong><p>Choose a thread from the inbox, or connect a service in settings.</p></div></div>';return}root.querySelector('#title').textContent=thread.title;root.querySelector('#participants').textContent=(thread.participants||[]).join(', ');root.querySelector('#badge').textContent=`${thread.channel} · ${thread.providerId}`;render(items)};
9
- host.setBusy=value=>{for(const control of composer.elements) control.disabled=Boolean(value)};host.setStatus=value=>status.textContent=String(value||'');
10
- composer.addEventListener('submit',event=>{event.preventDefault();const value=body.value.trim();if(!value)return;host.dispatchEvent(new CustomEvent('communication-send',{bubbles:true,composed:true,detail:{thread,body:value}}))});
11
- function render(items){messages.replaceChildren(...Array.from(items||[]).map(item=>{const article=document.createElement('article'),text=document.createElement('p'),footer=document.createElement('footer'),time=document.createElement('time'),state=document.createElement('span');article.className=`message ${item.direction}`;text.textContent=item.body||item.subject||'';time.dateTime=item.timestamp;time.textContent=new Intl.DateTimeFormat(undefined,{dateStyle:'short',timeStyle:'short'}).format(new Date(item.timestamp));state.textContent=item.status;footer.append(time,state);article.append(text,footer);const advisory=advisories.get(item);if(advisory){const note=document.createElement('aside'),title=document.createElement('strong'),summary=document.createElement('p');note.className='advisory';note.dataset.level=String(advisory.level||'caution');note.setAttribute('role','note');title.textContent=String(advisory.title||'Content advisory');summary.textContent=String(advisory.summary||'Review this message carefully.');note.append(title,summary);const signals=Array.from(advisory.signals||[]).map(String).filter(Boolean).slice(0,8);if(signals.length){const list=document.createElement('ul');for(const signal of signals){const entry=document.createElement('li');entry.textContent=signal;list.append(entry)}note.append(list)}if(advisory.actionLabel){const action=document.createElement('button');action.type='button';action.className='arcane-button arcane-button--tertiary';action.textContent=String(advisory.actionLabel);action.addEventListener('click',()=>host.dispatchEvent(new CustomEvent('communication-advisory-action',{bubbles:true,composed:true,detail:{thread,message:item,advisory}})));note.append(action)}article.append(note)}return article}));messages.scrollTop=messages.scrollHeight;}
12
- host.clearComposer=()=>{body.value='';body.focus()};host.ready=true;host.dispatchEvent(new CustomEvent('conversation-view-ready',{bubbles:true,composed:true}));
7
+ const host=this,root=host.shadowRoot,heading=root.querySelector('#heading'),messages=root.querySelector('#messages'),composer=root.querySelector('#composer'),body=root.querySelector('#body'),status=root.querySelector('#status');
8
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
9
+ const events=createArcaneEventSource(host,{source:'arcane.component.conversation-view',eventTypes:['communication-send','communication-advisory-action','conversation-view-ready']});
10
+ const lifecycleController=new AbortController();
11
+ let thread=null,advisories={},destroyed=false,operationSequence=0;
12
+ function nextOperationId(kind){operationSequence+=1;return `${events.descriptor.instanceId}:${kind}:${operationSequence}`}
13
+ 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})}
14
+ host.setConversation=(next,items=[],options={})=>{if(destroyed)return false;thread=next||null;advisories=options?.advisories instanceof Map?options.advisories:new Map();heading.hidden=!thread;composer.hidden=!thread;if(!thread){messages.innerHTML='<div class="blank"><div><strong>Select a conversation</strong><p>Choose a thread from the inbox, or connect a service in settings.</p></div></div>';return}root.querySelector('#title').textContent=thread.title;root.querySelector('#participants').textContent=(thread.participants||[]).join(', ');root.querySelector('#badge').textContent=`${thread.channel} · ${thread.providerId}`;render(items)};
15
+ host.setBusy=value=>{if(destroyed)return false;for(const control of composer.elements) control.disabled=Boolean(value)};host.setStatus=value=>{if(destroyed)return false;status.textContent=String(value||'')};
16
+ composer.addEventListener('submit',event=>{event.preventDefault();const value=body.value.trim();if(!value)return;emit('communication-send',{thread,body:value},{hasThread:Boolean(thread),bodyLength:value.length},nextOperationId('send'))},{signal:lifecycleController.signal});
17
+ function render(items){messages.replaceChildren(...Array.from(items||[]).map(item=>{const article=document.createElement('article'),text=document.createElement('p'),footer=document.createElement('footer'),time=document.createElement('time'),state=document.createElement('span');article.className=`message ${item.direction}`;text.textContent=item.body||item.subject||'';time.dateTime=item.timestamp;time.textContent=new Intl.DateTimeFormat(undefined,{dateStyle:'short',timeStyle:'short'}).format(new Date(item.timestamp));state.textContent=item.status;footer.append(time,state);article.append(text,footer);const advisory=advisories.get(item);if(advisory){const note=document.createElement('aside'),title=document.createElement('strong'),summary=document.createElement('p');note.className='advisory';note.dataset.level=String(advisory.level||'caution');note.setAttribute('role','note');title.textContent=String(advisory.title||'Content advisory');summary.textContent=String(advisory.summary||'Review this message carefully.');note.append(title,summary);const signals=Array.from(advisory.signals||[]).map(String).filter(Boolean).slice(0,8);if(signals.length){const list=document.createElement('ul');for(const signal of signals){const entry=document.createElement('li');entry.textContent=signal;list.append(entry)}note.append(list)}if(advisory.actionLabel){const action=document.createElement('button');action.type='button';action.className='arcane-button arcane-button--tertiary';action.textContent=String(advisory.actionLabel);action.addEventListener('click',()=>emit('communication-advisory-action',{thread,message:item,advisory},{hasThread:Boolean(thread),level:String(advisory.level||'caution')},nextOperationId('advisory-action')),{signal:lifecycleController.signal});note.append(action)}article.append(note)}return article}));messages.scrollTop=messages.scrollHeight;}
18
+ function destroy(){if(destroyed)return false;destroyed=true;lifecycleController.abort();thread=null;advisories={};events.dispose();host.ready=false;return true}
19
+ host.clearComposer=()=>{if(destroyed)return false;body.value='';body.focus()};host.destroy=destroy;host.ready=true;emit('conversation-view-ready',{}, {ready:true});
13
20
  </script>
@@ -23,6 +23,9 @@
23
23
  normalizeDashboardOptions,
24
24
  normalizeDashboardVisibility
25
25
  }=await import(moduleURL('ComponentContracts.js'));
26
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
27
+ const events=createArcaneEventSource(host,{source:'arcane.component.dashboard-config',eventTypes:['dashboard-config-closed','dashboard-config-opened','dashboard-config-change','dashboard-config-ready']});
28
+ const lifecycleController=new AbortController();
26
29
  const openButton=host.shadowRoot.querySelector('#openConfig');
27
30
  const configModal=host.shadowRoot.querySelector('#configModal');
28
31
 
@@ -30,9 +33,22 @@
30
33
  let visibility={};
31
34
  let content=null;
32
35
  let labels=normalizeDashboardOptions(optionsFromDataset()).labels;
36
+ let operationSequence=0;
37
+ let operationGeneration=0;
38
+ let destroyed=false;
39
+
40
+ function emit(type,detail={},publicDetail={},options={}){
41
+ if(destroyed){
42
+ return false;
43
+ }
44
+ const publication=events.dispatch(type,detail,{publicDetail,operationId:options.operationId||null,cancelable:Boolean(options.cancelable)});
45
+ if(options.cancelable&&!publication.accepted) return false;
46
+ return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true,cancelable:Boolean(options.cancelable)});
47
+ }
33
48
 
34
49
  host.close=closeConfig;
35
50
  host.configure=configure;
51
+ host.destroy=destroy;
36
52
  host.getChartOptions=getChartOptions;
37
53
  host.getEffectiveVisibility=getEffectiveVisibility;
38
54
  host.open=openConfig;
@@ -42,34 +58,33 @@
42
58
  Object.defineProperties(
43
59
  host,
44
60
  {
45
- definitions:{get:getDefinitions,set:setDefinitions},
46
- effectiveVisibility:{get:getEffectiveVisibility},
47
- options:{get:getOptions,set:configure},
48
- visibility:{get:getVisibility,set:setVisibility}
61
+ definitions:{configurable:true,get:getDefinitions,set:setDefinitions},
62
+ effectiveVisibility:{configurable:true,get:getEffectiveVisibility},
63
+ options:{configurable:true,get:getOptions,set:configure},
64
+ visibility:{configurable:true,get:getVisibility,set:setVisibility}
49
65
  }
50
66
  );
51
67
 
52
68
  openButton.addEventListener(
53
69
  'click',
54
70
  ()=>openConfig().catch(
55
- error=>console.error('Unable to configure dashboard:',error)
56
- )
71
+ error=>{
72
+ if(!destroyed){
73
+ console.error('Unable to configure dashboard:',error);
74
+ }
75
+ }
76
+ ),
77
+ {signal:lifecycleController.signal}
57
78
  );
58
79
  configModal.addEventListener(
59
80
  'modal-closed',
60
- ()=>host.dispatchEvent(
61
- new CustomEvent(
62
- 'dashboard-config-closed',
63
- {
64
- bubbles:true,
65
- composed:true,
66
- detail:{
67
- effectiveVisibility:getEffectiveVisibility(),
68
- visibility:getVisibility()
69
- }
70
- }
71
- )
72
- )
81
+ ()=>emit(
82
+ 'dashboard-config-closed',
83
+ {effectiveVisibility:getEffectiveVisibility(),visibility:getVisibility()},
84
+ {definitionCount:definitions.length},
85
+ {operationId:`${events.descriptor.instanceId}:close:${++operationSequence}`}
86
+ ),
87
+ {signal:lifecycleController.signal}
73
88
  );
74
89
 
75
90
  function optionsFromDataset(){
@@ -84,6 +99,9 @@
84
99
  }
85
100
 
86
101
  function configure(input={}){
102
+ if(destroyed){
103
+ return getOptions();
104
+ }
87
105
  const normalized=normalizeDashboardOptions(
88
106
  input,
89
107
  {definitions,labels,visibility}
@@ -97,6 +115,9 @@
97
115
  }
98
116
 
99
117
  function setDefinitions(values=[]){
118
+ if(destroyed){
119
+ return getDefinitions();
120
+ }
100
121
  definitions=normalizeDashboardDefinitions(values);
101
122
  renderTrigger();
102
123
  refreshContent();
@@ -104,6 +125,9 @@
104
125
  }
105
126
 
106
127
  function setVisibility(values={}){
128
+ if(destroyed){
129
+ return getVisibility();
130
+ }
107
131
  visibility=normalizeDashboardVisibility(values);
108
132
  syncOptions();
109
133
  return getVisibility();
@@ -143,50 +167,60 @@
143
167
  }
144
168
 
145
169
  async function openConfig(){
146
- if(!definitions.length){
170
+ if(destroyed||!definitions.length){
147
171
  return false;
148
172
  }
173
+ const generation=++operationGeneration;
149
174
  const modal=await waitForComponent(
150
175
  configModal,
151
176
  {
152
177
  methods:['populate','open'],
153
178
  property:'ready',
154
- event:'modal-ready'
179
+ event:'modal-ready',
180
+ signal:lifecycleController.signal
155
181
  }
156
182
  );
183
+ if(destroyed||generation!==operationGeneration){
184
+ return false;
185
+ }
157
186
  if(!content){
158
187
  content=createContent();
159
188
  await modal.populate(content,false);
189
+ if(destroyed||generation!==operationGeneration){
190
+ return false;
191
+ }
160
192
  }
161
193
  refreshContent();
162
- modal.open();
163
- host.dispatchEvent(
164
- new CustomEvent(
165
- 'dashboard-config-opened',
166
- {
167
- bubbles:true,
168
- composed:true,
169
- detail:{
170
- effectiveVisibility:getEffectiveVisibility(),
171
- visibility:getVisibility()
172
- }
173
- }
174
- )
175
- );
194
+ const opened=await modal.open();
195
+ if(destroyed||generation!==operationGeneration||opened!==true){
196
+ return false;
197
+ }
198
+ emit('dashboard-config-opened',{effectiveVisibility:getEffectiveVisibility(),visibility:getVisibility()},{definitionCount:definitions.length},{operationId:`${events.descriptor.instanceId}:open:${++operationSequence}`});
176
199
  return true;
177
200
  }
178
201
 
179
202
  async function closeConfig(){
203
+ if(destroyed){
204
+ return false;
205
+ }
206
+ const generation=++operationGeneration;
180
207
  const modal=await waitForComponent(
181
208
  configModal,
182
209
  {
183
210
  methods:['close'],
184
211
  property:'ready',
185
- event:'modal-ready'
212
+ event:'modal-ready',
213
+ signal:lifecycleController.signal
186
214
  }
187
215
  );
188
- modal.close();
189
- return true;
216
+ if(destroyed||generation!==operationGeneration){
217
+ return false;
218
+ }
219
+ const closed=await modal.close();
220
+ if(destroyed||generation!==operationGeneration){
221
+ return false;
222
+ }
223
+ return closed===true;
190
224
  }
191
225
 
192
226
  function createContent(){
@@ -205,7 +239,7 @@
205
239
  empty.dataset.dashboardConfigEmpty='';
206
240
  items.className='chart-options';
207
241
  items.dataset.dashboardConfigOptions='';
208
- items.addEventListener('change',changeVisibility);
242
+ items.addEventListener('change',changeVisibility,{signal:lifecycleController.signal});
209
243
  section.append(styles,heading,message,empty,items);
210
244
  return section;
211
245
  }
@@ -280,22 +314,14 @@
280
314
  const previous=isVisible(definition);
281
315
  const next={...visibility,[definition.key]:checkbox.checked};
282
316
  const effective=effectiveDashboardVisibility(definitions,next);
283
- const changeEvent=new CustomEvent(
317
+ operationSequence+=1;
318
+ const accepted=emit(
284
319
  'dashboard-config-change',
285
- {
286
- bubbles:true,
287
- cancelable:true,
288
- composed:true,
289
- detail:{
290
- definition:cloneDefinition(definition),
291
- effectiveVisibility:effective,
292
- key:definition.key,
293
- visibility:{...next},
294
- visible:checkbox.checked
295
- }
296
- }
320
+ {definition:cloneDefinition(definition),effectiveVisibility:effective,key:definition.key,visibility:{...next},visible:checkbox.checked},
321
+ {key:definition.key,visible:checkbox.checked},
322
+ {cancelable:true,operationId:`${events.descriptor.instanceId}:change:${operationSequence}`}
297
323
  );
298
- if(!host.dispatchEvent(changeEvent)){
324
+ if(!accepted){
299
325
  checkbox.checked=previous;
300
326
  return;
301
327
  }
@@ -330,12 +356,23 @@
330
356
  };
331
357
  }
332
358
 
359
+ function destroy(){
360
+ if(destroyed){
361
+ return false;
362
+ }
363
+ destroyed=true;
364
+ operationGeneration+=1;
365
+ lifecycleController.abort('dashboard-config-destroyed');
366
+ configModal.destroy?.();
367
+ definitions=[];
368
+ visibility={};
369
+ content=null;
370
+ events.dispose();
371
+ host.ready=false;
372
+ return true;
373
+ }
374
+
333
375
  renderTrigger();
334
376
  host.ready=true;
335
- host.dispatchEvent(
336
- new CustomEvent(
337
- 'dashboard-config-ready',
338
- {bubbles:true,composed:true}
339
- )
340
- );
377
+ emit('dashboard-config-ready',{}, {ready:true});
341
378
  </script>
@@ -18,34 +18,62 @@
18
18
  const maintenanceModal=host.shadowRoot.querySelector('#maintenanceModal');
19
19
  const {default:waitForComponent}=await import('../modules/WaitForComponent.js');
20
20
  const {clearEmptyChatsAndMemories}=await import('../modules/DataMaintenance.js?v=3');
21
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
22
+ const events=createArcaneEventSource(host,{source:'arcane.component.data-maintenance',eventTypes:['data-maintenance-complete','data-maintenance-ready']});
23
+ const lifecycleController=new AbortController();
21
24
 
22
25
  let content=null;
26
+ let openGeneration=0;
27
+ let clearGeneration=0;
28
+ let operationSequence=0;
29
+ let destroyed=false;
23
30
 
24
31
  host.open=openMaintenance;
32
+ host.destroy=destroy;
25
33
 
26
34
  openButton.addEventListener(
27
35
  'click',
28
36
  ()=>openMaintenance().catch(
29
- error=>console.error('Unable to open maintenance:',error)
30
- )
37
+ error=>{
38
+ if(!destroyed){
39
+ console.error('Unable to open maintenance:',error);
40
+ }
41
+ }
42
+ ),
43
+ {signal:lifecycleController.signal}
31
44
  );
32
45
 
33
46
  async function openMaintenance(){
47
+ if(destroyed){
48
+ return false;
49
+ }
50
+ const generation=++openGeneration;
34
51
  const modal=await waitForComponent(
35
52
  maintenanceModal,
36
53
  {
37
54
  methods:['populate','open'],
38
55
  property:'ready',
39
- event:'modal-ready'
56
+ event:'modal-ready',
57
+ signal:lifecycleController.signal
40
58
  }
41
59
  );
60
+ if(destroyed||generation!==openGeneration){
61
+ return false;
62
+ }
42
63
 
43
64
  if(!content){
44
65
  content=createContent();
45
66
  await modal.populate(content,false);
67
+ if(destroyed||generation!==openGeneration){
68
+ return false;
69
+ }
46
70
  }
47
71
 
48
- modal.open();
72
+ const opened=await modal.open();
73
+ if(destroyed||generation!==openGeneration){
74
+ return false;
75
+ }
76
+ return opened===true;
49
77
  }
50
78
 
51
79
  function createContent(){
@@ -64,11 +92,19 @@
64
92
  clearButton.addEventListener(
65
93
  'click',
66
94
  async function clearChats(){
95
+ if(destroyed){
96
+ return;
97
+ }
98
+ const generation=++clearGeneration;
99
+ const operationId=`${events.descriptor.instanceId}:clear:${++operationSequence}`;
67
100
  clearButton.disabled=true;
68
101
  status.innerText='Checking chat logs...';
69
102
 
70
103
  try{
71
104
  const result=await clearEmptyChatsAndMemories();
105
+ if(destroyed||generation!==clearGeneration){
106
+ return;
107
+ }
72
108
 
73
109
  if(result.deletedChats||result.deletedMemories){
74
110
  status.innerText=`Deleted ${result.deletedChats} empty ${result.deletedChats===1?'chat':'chats'} and ${result.deletedMemories} empty or associated ${result.deletedMemories===1?'memory':'memories'}.`;
@@ -80,19 +116,25 @@
80
116
  status.innerText+=` ${result.failed} could not be deleted.`;
81
117
  }
82
118
 
83
- host.dispatchEvent(
84
- new CustomEvent(
85
- 'data-maintenance-complete',
86
- {detail:result}
87
- )
119
+ const publication=events.dispatch(
120
+ 'data-maintenance-complete',
121
+ result,
122
+ {operationId,publicDetail:{deletedChats:Number(result.deletedChats||0),deletedMemories:Number(result.deletedMemories||0),failed:Number(result.failed||0)}}
88
123
  );
124
+ projectArcaneDOMEvent(host,publication.occurrence);
89
125
  }catch(error){
126
+ if(destroyed||generation!==clearGeneration){
127
+ return;
128
+ }
90
129
  status.innerText='Unable to complete data maintenance.';
91
130
  console.error('Unable to clear empty chats and memories:',error);
92
131
  }finally{
93
- clearButton.disabled=false;
132
+ if(!destroyed&&generation===clearGeneration){
133
+ clearButton.disabled=false;
134
+ }
94
135
  }
95
- }
136
+ },
137
+ {signal:lifecycleController.signal}
96
138
  );
97
139
 
98
140
  section.append(
@@ -105,8 +147,21 @@
105
147
  return section;
106
148
  }
107
149
 
150
+ function destroy(){
151
+ if(destroyed){
152
+ return false;
153
+ }
154
+ destroyed=true;
155
+ openGeneration+=1;
156
+ clearGeneration+=1;
157
+ lifecycleController.abort('data-maintenance-destroyed');
158
+ maintenanceModal.destroy?.();
159
+ events.dispose();
160
+ host.ready=false;
161
+ return true;
162
+ }
163
+
108
164
  host.ready=true;
109
- host.dispatchEvent(
110
- new CustomEvent('data-maintenance-ready')
111
- );
165
+ const readyPublication=events.dispatch('data-maintenance-ready',{}, {publicDetail:{ready:true}});
166
+ projectArcaneDOMEvent(host,readyPublication.occurrence);
112
167
  </script>
@@ -18,25 +18,43 @@
18
18
  const openButton=host.shadowRoot.querySelector('#openData');
19
19
  const dataModal=host.shadowRoot.querySelector('#dataModal');
20
20
  const {default:waitForComponent}=await import('../modules/WaitForComponent.js');
21
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
22
+ const events=createArcaneEventSource(host,{source:'arcane.component.data-view',eventTypes:['data-view-ready']});
23
+ const lifecycleController=new AbortController();
21
24
 
22
25
  let content=null;
23
26
  let fileManager=null;
27
+ let operationGeneration=0;
28
+ let destroyed=false;
24
29
 
25
30
  host.beforeOpen=typeof host.beforeOpen==='function'
26
31
  ?host.beforeOpen
27
32
  :async function beforeOpen(){};
28
33
  host.open=openData;
34
+ host.destroy=destroy;
29
35
 
30
36
  openButton.innerText=host.dataset.button||'View Data';
31
37
  openButton.addEventListener(
32
38
  'click',
33
39
  ()=>openData().catch(
34
- error=>console.error('Unable to open data view:',error)
35
- )
40
+ error=>{
41
+ if(!destroyed){
42
+ console.error('Unable to open data view:',error);
43
+ }
44
+ }
45
+ ),
46
+ {signal:lifecycleController.signal}
36
47
  );
37
48
 
38
49
  async function openData(){
50
+ if(destroyed){
51
+ return false;
52
+ }
53
+ const generation=++operationGeneration;
39
54
  await host.beforeOpen();
55
+ if(destroyed||generation!==operationGeneration){
56
+ return false;
57
+ }
40
58
 
41
59
  const reload=Boolean(content);
42
60
 
@@ -45,9 +63,13 @@
45
63
  {
46
64
  methods:['populate','open'],
47
65
  property:'ready',
48
- event:'modal-ready'
66
+ event:'modal-ready',
67
+ signal:lifecycleController.signal
49
68
  }
50
69
  );
70
+ if(destroyed||generation!==operationGeneration){
71
+ return false;
72
+ }
51
73
 
52
74
  if(!content){
53
75
  content=document.createElement('section');
@@ -67,6 +89,9 @@
67
89
 
68
90
  content.append(heading,fileManager);
69
91
  await modal.populate(content,false);
92
+ if(destroyed||generation!==operationGeneration){
93
+ return false;
94
+ }
70
95
  }
71
96
 
72
97
  const manager=await waitForComponent(
@@ -74,19 +99,40 @@
74
99
  {
75
100
  methods:['loadAll'],
76
101
  property:'ready',
77
- event:'file-manager-ready'
102
+ event:'file-manager-ready',
103
+ signal:lifecycleController.signal
78
104
  }
79
105
  );
106
+ if(destroyed||generation!==operationGeneration){
107
+ return false;
108
+ }
80
109
 
81
110
  if(reload){
82
111
  await manager.loadAll();
112
+ if(destroyed||generation!==operationGeneration){
113
+ return false;
114
+ }
83
115
  }
84
116
 
85
117
  modal.open();
118
+ return true;
119
+ }
120
+
121
+ function destroy(){
122
+ if(destroyed){
123
+ return false;
124
+ }
125
+ destroyed=true;
126
+ operationGeneration+=1;
127
+ lifecycleController.abort('data-view-destroyed');
128
+ fileManager?.destroy?.();
129
+ dataModal.destroy?.();
130
+ events.dispose();
131
+ host.ready=false;
132
+ return true;
86
133
  }
87
134
 
88
135
  host.ready=true;
89
- host.dispatchEvent(
90
- new CustomEvent('data-view-ready')
91
- );
136
+ const readyPublication=events.dispatch('data-view-ready',{}, {publicDetail:{ready:true}});
137
+ projectArcaneDOMEvent(host,readyPublication.occurrence);
92
138
  </script>