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
@@ -1,8 +1,10 @@
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}.widget{display:grid;gap:var(--arcane-space-4)}.current{align-items:center;display:grid;gap:var(--arcane-space-4);grid-template-columns:auto 1fr auto}.symbol{font-size:clamp(2.5rem,8vw,5rem);line-height:1}.temperature{font-size:clamp(2.2rem,7vw,4.5rem);font-weight:760;line-height:1}.location h2,.location p{margin:0}.location p,.details{color:var(--arcane-muted)}.details{display:grid;gap:var(--arcane-space-1);text-align:right}.forecast{display:grid;gap:var(--arcane-space-2);grid-template-columns:repeat(7,minmax(5.5rem,1fr));overflow:auto}.day{display:grid;gap:var(--arcane-space-2);padding:var(--arcane-space-3);text-align:center;background:var(--arcane-surface-muted);border:1px solid var(--arcane-border);border-radius:var(--arcane-radius-medium)}.day strong{white-space:nowrap}.day-symbol{font-size:1.8rem}.empty{padding:var(--arcane-space-6);text-align:center;color:var(--arcane-muted)}.footer{display:flex;align-items:center;justify-content:space-between;gap:var(--arcane-space-3)}@media(max-width:36rem){.current{grid-template-columns:auto 1fr}.details{grid-column:1/-1;text-align:left;grid-template-columns:repeat(3,1fr)}}</style>
3
3
  <section class="widget arcane-card"><div id="empty" class="empty"><strong>Weather is ready for a location</strong><p>Search for a city or postal code to load current conditions and the forecast.</p></div><div id="content" hidden><div class="arcane-card__body current"><span id="symbol" class="symbol" aria-hidden="true"></span><div class="location"><h2 id="location" class="arcane-section-heading"></h2><p id="condition"></p><div><span id="temperature" class="temperature"></span></div></div><div class="details"><span id="feels"></span><span id="humidity"></span><span id="wind"></span></div></div><div id="forecast" class="arcane-card__body forecast"></div><footer class="arcane-card__footer footer"><span id="source" class="arcane-help"></span><button id="refresh" class="arcane-button arcane-button--secondary" type="button">Refresh</button></footer></div></section>
4
- <script type="module">const host=this,root=host.shadowRoot,empty=root.querySelector('#empty'),content=root.querySelector('#content'),forecast=root.querySelector('#forecast');let weather=null;const conditions=new Map([[0,['Clear','☀️']],[1,['Mostly clear','🌤️']],[2,['Partly cloudy','⛅']],[3,['Overcast','☁️']],[45,['Fog','🌫️']],[48,['Freezing fog','🌫️']],[51,['Light drizzle','🌦️']],[53,['Drizzle','🌦️']],[55,['Heavy drizzle','🌧️']],[61,['Light rain','🌦️']],[63,['Rain','🌧️']],[65,['Heavy rain','🌧️']],[71,['Light snow','🌨️']],[73,['Snow','🌨️']],[75,['Heavy snow','❄️']],[80,['Rain showers','🌦️']],[81,['Rain showers','🌧️']],[82,['Heavy showers','⛈️']],[95,['Thunderstorm','⛈️']],[96,['Thunderstorm with hail','⛈️']],[99,['Severe thunderstorm','⛈️']]]);
5
- host.setWeather=setWeather;host.clear=()=>{weather=null;empty.hidden=false;content.hidden=true};root.querySelector('#refresh').addEventListener('click',()=>host.dispatchEvent(new CustomEvent('weather-refresh',{bubbles:true,composed:true,detail:{weather}})));
4
+ <script type="module">const host=this,root=host.shadowRoot,empty=root.querySelector('#empty'),content=root.querySelector('#content'),forecast=root.querySelector('#forecast');const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.weather-widget',eventTypes:['weather-refresh','weather-widget-ready']});let weather=null,destroyed=false;const conditions=new Map([[0,['Clear','☀️']],[1,['Mostly clear','🌤️']],[2,['Partly cloudy','⛅']],[3,['Overcast','☁️']],[45,['Fog','🌫️']],[48,['Freezing fog','🌫️']],[51,['Light drizzle','🌦️']],[53,['Drizzle','🌦️']],[55,['Heavy drizzle','🌧️']],[61,['Light rain','🌦️']],[63,['Rain','🌧️']],[65,['Heavy rain','🌧️']],[71,['Light snow','🌨️']],[73,['Snow','🌨️']],[75,['Heavy snow','❄️']],[80,['Rain showers','🌦️']],[81,['Rain showers','🌧️']],[82,['Heavy showers','⛈️']],[95,['Thunderstorm','⛈️']],[96,['Thunderstorm with hail','⛈️']],[99,['Severe thunderstorm','⛈️']]]);
5
+ function emit(type,detail={},publicDetail={}){if(destroyed)return false;const publication=events.dispatch(type,detail,{publicDetail});return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true})}
6
+ host.destroy=()=>{if(destroyed)return false;destroyed=true;host.ready=false;events.dispose();return true};
7
+ host.setWeather=setWeather;host.clear=()=>{weather=null;empty.hidden=false;content.hidden=true};root.querySelector('#refresh').addEventListener('click',()=>emit('weather-refresh',{weather},{hasWeather:Boolean(weather)}));
6
8
  function info(code,isDay=true){const value=conditions.get(Number(code))||['Conditions unavailable','🌡️'];if(!isDay&&Number(code)<=1)return ['Clear night','🌙'];return value}
7
9
  function setWeather(value){weather=value;if(!value)return host.clear();empty.hidden=true;content.hidden=false;const current=value.current,[label,symbol]=info(current.weatherCode,current.isDay);root.querySelector('#symbol').textContent=symbol;root.querySelector('#location').textContent=[value.location.name,value.location.region,value.location.country].filter(Boolean).join(', ');root.querySelector('#condition').textContent=label;root.querySelector('#temperature').textContent=`${Math.round(current.temperature)}${current.temperatureUnit}`;root.querySelector('#feels').textContent=`Feels ${Math.round(current.apparentTemperature)}${current.temperatureUnit}`;root.querySelector('#humidity').textContent=`Humidity ${Math.round(current.humidity)}%`;root.querySelector('#wind').textContent=`Wind ${Math.round(current.windSpeed)} ${current.windUnit}`;root.querySelector('#source').textContent=`${value.source||'Weather source'} · updated ${new Intl.DateTimeFormat(undefined,{timeStyle:'short'}).format(new Date(value.fetchedAt))}`;forecast.replaceChildren(...Array.from(value.daily||[]).map(day=>{const section=document.createElement('section'),name=document.createElement('strong'),icon=document.createElement('span'),temperatures=document.createElement('span'),rain=document.createElement('span');section.className='day';name.textContent=new Intl.DateTimeFormat(undefined,{weekday:'short'}).format(new Date(day.date));icon.className='day-symbol';icon.textContent=info(day.weatherCode)[1];temperatures.textContent=`${Math.round(day.temperatureMax)}° / ${Math.round(day.temperatureMin)}°`;rain.textContent=`${Math.round(day.precipitationProbability)}% rain`;section.append(name,icon,temperatures,rain);return section}));return weather}
8
- host.ready=true;host.dispatchEvent(new CustomEvent('weather-widget-ready',{bubbles:true,composed:true}));</script>
10
+ host.ready=true;emit('weather-widget-ready',{}, {ready:true});</script>
@@ -61,6 +61,8 @@ button{min-width:2.5rem}
61
61
  <script type="module">
62
62
  const host=this;
63
63
  const root=host.shadowRoot;
64
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
65
+ const events=createArcaneEventSource(host,{source:'arcane.component.web-navigator',eventTypes:['web-navigation-blocked','web-navigate','web-open-external','web-navigator-ready']});
64
66
  const address=root.querySelector('#address');
65
67
  const frame=root.querySelector('#frame');
66
68
  const blocked=root.querySelector('#blocked');
@@ -72,6 +74,20 @@ let history=[];
72
74
  let position=-1;
73
75
  let navigationGuard=null;
74
76
  let navigationSequence=0;
77
+ let eventOperationSequence=0;
78
+ let destroyed=false;
79
+
80
+ function emit(type,detail={},publicDetail={}){
81
+ if(destroyed)return false;
82
+ const operationId=
83
+ `${events.instanceId}:${type}:${(++eventOperationSequence).toString(36)}`;
84
+ const publication=events.dispatch(
85
+ type,
86
+ detail,
87
+ {operationId,publicDetail:{...publicDetail,reason:type}}
88
+ );
89
+ return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
90
+ }
75
91
 
76
92
  function normalize(value){
77
93
  let text=String(value||'').trim();
@@ -143,7 +159,7 @@ function showBlocked(url,decision){
143
159
  root.querySelector('#blockedEvidence').textContent=[decision.ruleId&&`Rule ${decision.ruleId}`,decision.policyGeneration&&`Policy generation ${decision.policyGeneration}`,decision.decisionId&&`Decision ${decision.decisionId}`,decision.decidedAt].filter(Boolean).join(' · ');
144
160
  status.textContent='Blocked by the Arcane global deny policy.';
145
161
  blocked.focus();
146
- host.dispatchEvent(new CustomEvent('web-navigation-blocked',{bubbles:true,composed:true,detail:{url,decision}}));
162
+ emit('web-navigation-blocked',{url,decision},{blocked:true,ruleId:String(decision.ruleId||''),policyGeneration:decision.policyGeneration??null});
147
163
  }
148
164
 
149
165
  function showFrame(url){
@@ -151,17 +167,18 @@ function showFrame(url){
151
167
  frame.hidden=false;
152
168
  frame.src=url;
153
169
  status.textContent='Loading…';
154
- host.dispatchEvent(new CustomEvent('web-navigate',{bubbles:true,composed:true,detail:{url}}));
170
+ emit('web-navigate',{url},{intent:'embedded'});
155
171
  }
156
172
 
157
173
  async function navigate(value,{record=true}={}){
174
+ if(destroyed)return '';
158
175
  const sequence=++navigationSequence;
159
176
  try{
160
177
  const url=normalize(value);
161
178
  address.value=url;
162
179
  status.textContent='Checking the global deny policy…';
163
180
  const decision=await evaluate(url,'embedded');
164
- if(sequence!==navigationSequence)return'';
181
+ if(destroyed||sequence!==navigationSequence)return'';
165
182
  recordHistory(url,record);
166
183
  updateButtons();
167
184
  if(decision.blocked){
@@ -177,26 +194,29 @@ async function navigate(value,{record=true}={}){
177
194
  }
178
195
 
179
196
  async function openExternal(){
197
+ if(destroyed)return false;
180
198
  const sequence=++navigationSequence;
181
199
  try{
182
200
  const url=normalize(currentUrl()||address.value||homeUrl);
183
201
  status.textContent='Checking the global deny policy…';
184
202
  const decision=await evaluate(url,'external');
185
- if(sequence!==navigationSequence)return;
203
+ if(destroyed||sequence!==navigationSequence)return false;
186
204
  if(decision.blocked){
187
205
  showBlocked(url,decision);
188
206
  return;
189
207
  }
190
208
  status.textContent='Handing the allowed address to the system browser…';
191
- host.dispatchEvent(new CustomEvent('web-open-external',{bubbles:true,composed:true,detail:{url}}));
209
+ emit('web-open-external',{url},{intent:'external'});
210
+ return true;
192
211
  }catch(error){
193
- if(sequence===navigationSequence)status.textContent=error.message;
212
+ if(!destroyed&&sequence===navigationSequence)status.textContent=error.message;
213
+ return false;
194
214
  }
195
215
  }
196
216
 
197
217
  function ownNavigation(operation){
198
218
  operation.catch(function reportUnexpectedNavigationFailure(error){
199
- status.textContent=error.message;
219
+ if(!destroyed)status.textContent=error.message;
200
220
  });
201
221
  }
202
222
 
@@ -208,6 +228,7 @@ function move(amount){
208
228
  }
209
229
 
210
230
  function configure(options){
231
+ if(destroyed)return false;
211
232
  if(options?.homeUrl)homeUrl=normalize(options.homeUrl);
212
233
  navigationGuard=typeof options?.navigationGuard==='function'?options.navigationGuard:null;
213
234
  if(options?.initialUrl)ownNavigation(navigate(options.initialUrl));
@@ -223,7 +244,26 @@ function handleBackClick(){move(-1);}
223
244
  function handleForwardClick(){move(1);}
224
245
  function handleFrameLoad(){if(blocked.hidden)status.textContent='Loaded. If the page is blank, its site does not permit embedded viewing.';}
225
246
 
247
+ function destroy(){
248
+ if(destroyed)return false;
249
+ destroyed=true;
250
+ navigationSequence+=1;
251
+ navigationGuard=null;
252
+ host.ready=false;
253
+ root.querySelector('#addressForm').removeEventListener('submit',handleAddressSubmit);
254
+ root.querySelector('#home').removeEventListener('click',handleHomeClick);
255
+ root.querySelector('#reload').removeEventListener('click',handleReloadClick);
256
+ root.querySelector('#external').removeEventListener('click',handleExternalClick);
257
+ back.removeEventListener('click',handleBackClick);
258
+ forward.removeEventListener('click',handleForwardClick);
259
+ frame.removeEventListener('load',handleFrameLoad);
260
+ frame.removeAttribute('src');
261
+ events.dispose();
262
+ return true;
263
+ }
264
+
226
265
  host.configure=configure;
266
+ host.destroy=destroy;
227
267
  host.navigate=navigate;
228
268
  host.currentUrl=currentUrl;
229
269
  root.querySelector('#addressForm').addEventListener('submit',handleAddressSubmit);
@@ -235,5 +275,5 @@ forward.addEventListener('click',handleForwardClick);
235
275
  frame.addEventListener('load',handleFrameLoad);
236
276
  updateButtons();
237
277
  host.ready=true;
238
- host.dispatchEvent(new CustomEvent('web-navigator-ready',{bubbles:true,composed:true}));
278
+ emit('web-navigator-ready',{}, {ready:true});
239
279
  </script>
@@ -1,6 +1,6 @@
1
1
  import Is from '../../node_modules/strong-type/index.js';
2
2
  import '../modules/DBOPFS.js';
3
- import '../modules/AI.js?v=8';
3
+ import '../modules/AI.js';
4
4
  import {hasUserEntry} from '../modules/ChatRecords.js';
5
5
  import {normalizeMemoryContent} from '../modules/MemoryRecords.js';
6
6
 
@@ -1,5 +1,10 @@
1
1
  import Is from '../../node_modules/strong-type/index.js';
2
2
  import DBLS from '../modules/DBLS.js';
3
+ import {
4
+ arcaneEvents,
5
+ createArcaneEventSource,
6
+ projectArcaneDOMEvent
7
+ } from 'arcane-os/event-manager';
3
8
 
4
9
  /**
5
10
  * DBOPFS Module
@@ -47,6 +52,17 @@ const is = new Is(false);
47
52
  const EMAIL_REGEX =
48
53
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
49
54
 
55
+ const USER_ENTITY_LOADED_EVENT='user-entity-loaded';
56
+ const USER_ENTITY_LOADED_REASON='user-data-loaded';
57
+ let singletonDBOPFSReadyUnsubscribe=null;
58
+
59
+ function userEntityLifecycleError(code,reason,message){
60
+ const error=new Error(message);
61
+ error.code=code;
62
+ error.reason=reason;
63
+ return error;
64
+ }
65
+
50
66
 
51
67
  /**
52
68
  * Canonical schema for a UserEntity record.
@@ -85,6 +101,16 @@ const EMAIL_REGEX =
85
101
 
86
102
  class UserEntity {
87
103
 
104
+ #events;
105
+
106
+ #disposed = false;
107
+
108
+ #loadGeneration = 0;
109
+
110
+ #operationSequence = 0;
111
+
112
+ #stopDBOPFSReady = null;
113
+
88
114
  /** @type {string} */
89
115
  #tableName = 'users';
90
116
 
@@ -192,14 +218,22 @@ class UserEntity {
192
218
  }
193
219
 
194
220
  this.fileName = fileName;
195
-
196
- window.addEventListener(
197
- 'dbopfs-ready',
198
- this.load.bind(this)
199
- );
221
+ this.#events=createArcaneEventSource(this,{
222
+ source:'user-entity',
223
+ eventTypes:Object.freeze([USER_ENTITY_LOADED_EVENT])
224
+ });
200
225
 
201
226
  if(window.dbopfs?.ready){
202
227
  this.load();
228
+ }else{
229
+ this.#stopDBOPFSReady=arcaneEvents.subscribe(
230
+ 'dbopfs-ready',
231
+ ()=>{
232
+ this.#stopDBOPFSReady=null;
233
+ this.load();
234
+ },
235
+ {once:true}
236
+ );
203
237
  }
204
238
 
205
239
  return this;
@@ -912,6 +946,14 @@ class UserEntity {
912
946
  * only await if you need to ensure data is loaded before proceeding
913
947
  */
914
948
  async load(){
949
+ if(this.#disposed){
950
+ throw userEntityLifecycleError(
951
+ 'ARCANE_USER_ENTITY_DISPOSED',
952
+ 'user-entity-disposed',
953
+ 'The user entity has been disposed.'
954
+ );
955
+ }
956
+
915
957
  if(this.ready){
916
958
  return this.explicit;
917
959
  }
@@ -920,12 +962,17 @@ class UserEntity {
920
962
  return this.#loadPromise;
921
963
  }
922
964
 
923
- this.#loadPromise=this.#load();
965
+ const generation=++this.#loadGeneration;
966
+ const operationId=`${this.#events.instanceId}:load:${(++this.#operationSequence).toString(36)}`;
967
+ const loadPromise=this.#load(generation,operationId);
968
+ this.#loadPromise=loadPromise;
924
969
 
925
970
  try{
926
- return await this.#loadPromise;
971
+ return await loadPromise;
927
972
  }finally{
928
- this.#loadPromise=null;
973
+ if(this.#loadPromise===loadPromise){
974
+ this.#loadPromise=null;
975
+ }
929
976
  }
930
977
  }
931
978
 
@@ -956,12 +1003,26 @@ class UserEntity {
956
1003
  return this.explicit;
957
1004
  }
958
1005
 
959
- async #load(){
1006
+ async #load(generation,operationId){
960
1007
  const user=await dbopfs.get(
961
1008
  this.#tableName,
962
1009
  this.fileName
963
1010
  );
964
1011
 
1012
+ if(this.#disposed||generation!==this.#loadGeneration){
1013
+ throw userEntityLifecycleError(
1014
+ this.#disposed
1015
+ ?'ARCANE_USER_ENTITY_DISPOSED'
1016
+ :'ARCANE_USER_ENTITY_LOAD_SUPERSEDED',
1017
+ this.#disposed
1018
+ ?'user-entity-disposed'
1019
+ :'user-entity-load-superseded',
1020
+ this.#disposed
1021
+ ?'The user entity was disposed before its data load settled.'
1022
+ :'The user entity data load was superseded before settlement.'
1023
+ );
1024
+ }
1025
+
965
1026
  if(user){
966
1027
  const persist=this.persist;
967
1028
  this.persist=false;
@@ -975,16 +1036,21 @@ class UserEntity {
975
1036
 
976
1037
  this.ready=true;
977
1038
 
978
- window.dispatchEvent(
979
- new CustomEvent(
980
- 'user-entity-loaded',
981
- {
982
- detail:{
983
- user:this
984
- }
985
- }
986
- )
1039
+ const {occurrence}=this.#events.dispatch(
1040
+ USER_ENTITY_LOADED_EVENT,
1041
+ Object.freeze({
1042
+ reason:USER_ENTITY_LOADED_REASON,
1043
+ user:this
1044
+ }),
1045
+ {
1046
+ operationId,
1047
+ publicDetail:Object.freeze({
1048
+ ready:true,
1049
+ reason:USER_ENTITY_LOADED_REASON
1050
+ })
1051
+ }
987
1052
  );
1053
+ projectArcaneDOMEvent(window,occurrence);
988
1054
 
989
1055
  return this.explicit;
990
1056
  }
@@ -1025,18 +1091,39 @@ class UserEntity {
1025
1091
 
1026
1092
  return true;
1027
1093
  }
1028
- }
1029
1094
 
1030
- window.addEventListener(
1031
- 'dbopfs-ready',
1032
- initSingletonUserEntity
1033
- );
1095
+ dispose(){
1096
+ if(this.#disposed)return false;
1097
+ this.#disposed=true;
1098
+ this.#loadGeneration+=1;
1099
+ this.ready=false;
1100
+ this.#stopDBOPFSReady?.();
1101
+ this.#stopDBOPFSReady=null;
1102
+ if(window.user===this){
1103
+ delete window.user;
1104
+ singletonDBOPFSReadyUnsubscribe?.();
1105
+ singletonDBOPFSReadyUnsubscribe=null;
1106
+ }
1107
+ return this.#events.dispose();
1108
+ }
1109
+
1110
+ destroy(){
1111
+ return this.dispose();
1112
+ }
1113
+ }
1034
1114
 
1035
1115
  if(window.dbopfs?.ready){
1036
1116
  initSingletonUserEntity();
1117
+ }else{
1118
+ singletonDBOPFSReadyUnsubscribe=arcaneEvents.subscribe(
1119
+ 'dbopfs-ready',
1120
+ initSingletonUserEntity,
1121
+ {once:true}
1122
+ );
1037
1123
  }
1038
1124
 
1039
1125
  function initSingletonUserEntity(){
1126
+ singletonDBOPFSReadyUnsubscribe=null;
1040
1127
  if(!window.user){
1041
1128
  window.user = new UserEntity();
1042
1129
  }