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.
- package/CHANGELOG.md +29 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
- package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
- package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
- package/browser-runtime/ai/browser-speech.mjs +2 -0
- package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
- package/browser-runtime/ai/model-controller.mjs +285 -95
- package/browser-runtime/ai/speech-worker-client.mjs +247 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
- package/browser-runtime/event-manager.mjs +1097 -1
- package/docs/architecture.md +2 -2
- package/docs/event-manager.md +155 -27
- package/docs/reference/README.md +27 -27
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1162 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +6 -3
- package/docs/reference/behavioral-testing.md +29 -6
- package/docs/reference/cli.md +117 -9
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/event-manager.md +577 -32
- package/docs/reference/inventory/package-api.json +478 -2
- package/docs/reference/inventory/runtime-components.json +108 -44
- package/docs/reference/inventory/runtime-modules.json +131 -53
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +157 -43
- package/docs/reference/runtime-components.md +258 -83
- package/docs/reference/runtime-modules.md +613 -77
- package/docs/reference/sdk-api.md +1014 -25
- package/package.json +5 -4
- package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
- package/runtime/arcane/components/app-bar.html +34 -13
- package/runtime/arcane/components/assistant-panel.html +110 -57
- package/runtime/arcane/components/calculator.html +7 -4
- package/runtime/arcane/components/chart.html +58 -17
- package/runtime/arcane/components/chat.html +606 -136
- package/runtime/arcane/components/conversation-view.html +13 -6
- package/runtime/arcane/components/dashboard-config.html +96 -59
- package/runtime/arcane/components/data-maintenance.html +69 -14
- package/runtime/arcane/components/data-view.html +53 -7
- package/runtime/arcane/components/directory-picker.html +118 -32
- package/runtime/arcane/components/document-inspector.html +47 -10
- package/runtime/arcane/components/file-drop.html +81 -35
- package/runtime/arcane/components/file-inspector.html +72 -22
- package/runtime/arcane/components/file-manager.html +374 -79
- package/runtime/arcane/components/integration-settings.html +12 -5
- package/runtime/arcane/components/local-ai-status.html +48 -19
- package/runtime/arcane/components/markdown-document.html +161 -68
- package/runtime/arcane/components/markdown-editor.html +110 -33
- package/runtime/arcane/components/media-embed.html +8 -5
- package/runtime/arcane/components/modal.html +15 -5
- package/runtime/arcane/components/output-panel.html +28 -23
- package/runtime/arcane/components/preferences-form.html +22 -4
- package/runtime/arcane/components/record-timeline.html +18 -2
- package/runtime/arcane/components/relationship-board.html +23 -3
- package/runtime/arcane/components/screen-capture.html +10 -4
- package/runtime/arcane/components/source-code-viewer.html +76 -9
- package/runtime/arcane/components/source-explanation.html +23 -3
- package/runtime/arcane/components/speech.html +462 -384
- package/runtime/arcane/components/summary-strip.html +22 -11
- package/runtime/arcane/components/table.html +39 -21
- package/runtime/arcane/components/task-progress.html +79 -21
- package/runtime/arcane/components/terminal-workspace.html +7 -4
- package/runtime/arcane/components/theme-editor.html +7 -3
- package/runtime/arcane/components/unified-inbox.html +9 -4
- package/runtime/arcane/components/voice-transcription.html +639 -98
- package/runtime/arcane/components/weather-widget.html +5 -3
- package/runtime/arcane/components/web-navigator.html +48 -8
- package/runtime/arcane/entities/Chat.js +1 -1
- package/runtime/arcane/entities/User.js +110 -23
- package/runtime/arcane/modules/AI.js +2109 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
- package/runtime/arcane/modules/AIRuntimeState.js +109 -52
- package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
- package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
- package/runtime/arcane/modules/CalculatorEngine.js +63 -3
- package/runtime/arcane/modules/CommunicationAppController.js +588 -28
- package/runtime/arcane/modules/CommunicationHub.js +590 -10
- package/runtime/arcane/modules/ComponentContracts.js +470 -0
- package/runtime/arcane/modules/ConversationTimebox.js +152 -33
- package/runtime/arcane/modules/DBLS.js +40 -7
- package/runtime/arcane/modules/DBOPFS.js +35 -11
- package/runtime/arcane/modules/DataMaintenance.js +12 -2
- package/runtime/arcane/modules/Errors.js +65 -7
- package/runtime/arcane/modules/HTMLImport.js +198 -14
- package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
- package/runtime/arcane/modules/Mail.js +738 -115
- package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
- package/runtime/arcane/modules/MailTransport.mjs +197 -39
- package/runtime/arcane/modules/Ollama.js +36 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
- package/runtime/arcane/modules/PreferenceStore.js +367 -33
- package/runtime/arcane/modules/RecordReviewStore.js +322 -23
- package/runtime/arcane/modules/ScreenCapture.js +1397 -15
- package/runtime/arcane/modules/SpeechPlayback.js +438 -41
- package/runtime/arcane/modules/TerminalClient.js +277 -12
- package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
- package/runtime/arcane/modules/ThemeManager.js +39 -7
- package/runtime/arcane/modules/TimeGuard.js +131 -20
- package/runtime/arcane/modules/WaitForComponent.js +386 -33
- package/schemas/arcane-lock.schema.json +2 -2
- package/src/cli/main.mjs +435 -9
- package/src/event-manager.mjs +1097 -1
- package/src/import-map.mjs +21 -3
- package/src/index.mjs +13 -0
- package/src/installed-sdk-runtime.mjs +112 -0
- package/src/mail-api.mjs +22 -0
- package/src/mail-credentials.mjs +667 -0
- package/src/mail-server.mjs +1769 -0
- package/src/mail.mjs +261 -0
- package/src/sdk-browser-runtime.mjs +85 -41
- package/src/testing-loader.mjs +7 -0
- package/src/toolchain.mjs +3 -0
- 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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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.#
|
|
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
|
|
971
|
+
return await loadPromise;
|
|
927
972
|
}finally{
|
|
928
|
-
this.#loadPromise
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
}
|