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,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createArcaneEventSource,
|
|
3
|
+
projectArcaneDOMEvent
|
|
4
|
+
} from 'arcane-os/event-manager';
|
|
5
|
+
|
|
1
6
|
const htmlImportHostRegistryKey=Symbol.for('arcane.html-import.hosts');
|
|
2
7
|
const htmlImportHostRegistry=globalThis[htmlImportHostRegistryKey] instanceof Map
|
|
3
8
|
?globalThis[htmlImportHostRegistryKey]
|
|
@@ -7,7 +12,24 @@ globalThis[htmlImportHostRegistryKey]=htmlImportHostRegistry;
|
|
|
7
12
|
|
|
8
13
|
let htmlImportScriptId=0;
|
|
9
14
|
|
|
15
|
+
function samePropertyDescriptor(left,right){
|
|
16
|
+
if(!left||!right)return left===right;
|
|
17
|
+
return left.configurable===right.configurable
|
|
18
|
+
&&left.enumerable===right.enumerable
|
|
19
|
+
&&left.writable===right.writable
|
|
20
|
+
&&left.value===right.value
|
|
21
|
+
&&left.get===right.get
|
|
22
|
+
&&left.set===right.set;
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
class HTMLImport extends HTMLElement {
|
|
26
|
+
#connectionGeneration=0;
|
|
27
|
+
#eventOwner=Object.freeze({kind:'html-import-lifecycle-owner'});
|
|
28
|
+
#events;
|
|
29
|
+
#importedDestroy=null;
|
|
30
|
+
#loadController=null;
|
|
31
|
+
#loadTask=Promise.resolve();
|
|
32
|
+
|
|
11
33
|
ready=false;
|
|
12
34
|
|
|
13
35
|
constructor() {
|
|
@@ -15,12 +37,40 @@ class HTMLImport extends HTMLElement {
|
|
|
15
37
|
this.attachShadow({ mode: 'open' });
|
|
16
38
|
}
|
|
17
39
|
|
|
18
|
-
|
|
40
|
+
#createEvents(){
|
|
41
|
+
this.#events=createArcaneEventSource(this.#eventOwner,{
|
|
42
|
+
source:'html-import',
|
|
43
|
+
eventTypes:Object.freeze([
|
|
44
|
+
'html-import-error',
|
|
45
|
+
'html-import-ready'
|
|
46
|
+
])
|
|
47
|
+
});
|
|
48
|
+
return this.#events;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
connectedCallback() {
|
|
52
|
+
const generation=++this.#connectionGeneration;
|
|
53
|
+
const controller=new AbortController();
|
|
19
54
|
this.ready=false;
|
|
55
|
+
this.#loadController?.abort('html-import-load-superseded');
|
|
56
|
+
this.#loadController=controller;
|
|
57
|
+
const previous=this.#loadTask;
|
|
58
|
+
const task=Promise.resolve(previous)
|
|
59
|
+
.catch(()=>{})
|
|
60
|
+
.then(()=>this.#connect(generation,controller));
|
|
61
|
+
this.#loadTask=task;
|
|
62
|
+
return task;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async #connect(generation,controller){
|
|
66
|
+
if(!this.#isCurrentConnection(generation,controller))return;
|
|
67
|
+
if(!this.#events||this.#events.disposed)this.#createEvents();
|
|
20
68
|
const href=this.getAttribute('href');
|
|
21
69
|
let resolvedHref='';
|
|
70
|
+
const operationId=`html-import-load-${this.#events.instanceId}-${generation}`;
|
|
22
71
|
if(!href){
|
|
23
72
|
console.log('no href provided for html-import tag',this);
|
|
73
|
+
this.#loadController=null;
|
|
24
74
|
return;
|
|
25
75
|
}
|
|
26
76
|
|
|
@@ -35,46 +85,167 @@ class HTMLImport extends HTMLElement {
|
|
|
35
85
|
cache:'default',
|
|
36
86
|
credentials:'same-origin',
|
|
37
87
|
method:'GET',
|
|
38
|
-
redirect:'error'
|
|
88
|
+
redirect:'error',
|
|
89
|
+
signal:controller.signal
|
|
39
90
|
});
|
|
40
|
-
|
|
91
|
+
if(!this.#isCurrentConnection(generation,controller))return;
|
|
92
|
+
await this.#loadHTML(href,resolvedHref,response,generation,controller,operationId);
|
|
41
93
|
}catch(err){
|
|
94
|
+
if(!this.#isCurrentConnection(generation,controller)){
|
|
95
|
+
await this.#destroyImportedHost();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
42
98
|
console.error('Error loading HTML component:',err);
|
|
43
|
-
|
|
44
|
-
bubbles:true,
|
|
45
|
-
composed:true,
|
|
46
|
-
detail:{
|
|
99
|
+
const detail=Object.freeze({
|
|
47
100
|
code:'HTML_IMPORT_FAILED',
|
|
48
101
|
href,
|
|
49
102
|
message:'The component could not be loaded.',
|
|
103
|
+
reason:'component-import-rejected',
|
|
50
104
|
resolvedHref
|
|
105
|
+
});
|
|
106
|
+
const {occurrence}=this.#events.dispatch(
|
|
107
|
+
'html-import-error',
|
|
108
|
+
detail,
|
|
109
|
+
{
|
|
110
|
+
operationId,
|
|
111
|
+
publicDetail:Object.freeze({code:detail.code,reason:detail.reason})
|
|
51
112
|
}
|
|
52
|
-
|
|
113
|
+
);
|
|
114
|
+
projectArcaneDOMEvent(this,occurrence,{
|
|
115
|
+
bubbles:true,
|
|
116
|
+
composed:true
|
|
117
|
+
});
|
|
118
|
+
await this.#destroyImportedHost();
|
|
119
|
+
}finally{
|
|
120
|
+
if(this.#loadController===controller)this.#loadController=null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
disconnectedCallback(){
|
|
125
|
+
this.ready=false;
|
|
126
|
+
++this.#connectionGeneration;
|
|
127
|
+
this.#loadController?.abort('html-import-disconnected');
|
|
128
|
+
this.#loadController=null;
|
|
129
|
+
const events=this.#events;
|
|
130
|
+
const previous=this.#loadTask;
|
|
131
|
+
const immediateTeardown=this.#destroyImportedHost();
|
|
132
|
+
const task=Promise.allSettled([previous,immediateTeardown]).then(()=>{
|
|
133
|
+
events?.dispose();
|
|
134
|
+
if(this.#events===events)this.#events=null;
|
|
135
|
+
});
|
|
136
|
+
this.#loadTask=task;
|
|
137
|
+
return task;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#isCurrentConnection(generation,controller){
|
|
141
|
+
return this.isConnected
|
|
142
|
+
&&generation===this.#connectionGeneration
|
|
143
|
+
&&this.#loadController===controller
|
|
144
|
+
&&!controller.signal.aborted;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#reportImportedDestroyError(error,{events,href,operationId}){
|
|
148
|
+
console.error('Error destroying imported HTML component:',error);
|
|
149
|
+
if(!events||events.disposed)return;
|
|
150
|
+
const detail=Object.freeze({
|
|
151
|
+
code:'HTML_IMPORT_IMPORTED_COMPONENT_DESTROY_REJECTED',
|
|
152
|
+
href,
|
|
153
|
+
message:'The imported component could not be destroyed.',
|
|
154
|
+
reason:'imported-component-destroy-rejected'
|
|
155
|
+
});
|
|
156
|
+
const {occurrence}=events.dispatch(
|
|
157
|
+
'html-import-error',
|
|
158
|
+
detail,
|
|
159
|
+
{
|
|
160
|
+
operationId,
|
|
161
|
+
publicDetail:Object.freeze({code:detail.code,reason:detail.reason})
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
projectArcaneDOMEvent(this,occurrence,{
|
|
165
|
+
bubbles:true,
|
|
166
|
+
composed:true
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#restoreImportedDestroy(record){
|
|
171
|
+
const current=Object.getOwnPropertyDescriptor(this,'destroy')??null;
|
|
172
|
+
if(!samePropertyDescriptor(current,record.installedDescriptor))return false;
|
|
173
|
+
if(record.previousDescriptor){
|
|
174
|
+
Object.defineProperty(this,'destroy',record.previousDescriptor);
|
|
175
|
+
}else{
|
|
176
|
+
delete this.destroy;
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#destroyImportedHost(){
|
|
182
|
+
const record=this.#importedDestroy;
|
|
183
|
+
if(!record)return Promise.resolve(false);
|
|
184
|
+
this.#importedDestroy=null;
|
|
185
|
+
const events=this.#events;
|
|
186
|
+
const generation=this.#connectionGeneration;
|
|
187
|
+
const reportContext={
|
|
188
|
+
events,
|
|
189
|
+
href:this.getAttribute('href')||'',
|
|
190
|
+
operationId:events
|
|
191
|
+
?`html-import-destroy-${events.instanceId}-${generation}`
|
|
192
|
+
:null
|
|
193
|
+
};
|
|
194
|
+
let result;
|
|
195
|
+
try{
|
|
196
|
+
result=record.destroy.call(this);
|
|
197
|
+
}catch(error){
|
|
198
|
+
this.#reportImportedDestroyError(error,reportContext);
|
|
199
|
+
this.#restoreImportedDestroy(record);
|
|
200
|
+
return Promise.resolve(false);
|
|
53
201
|
}
|
|
202
|
+
return Promise.resolve(result)
|
|
203
|
+
.then(
|
|
204
|
+
()=>true,
|
|
205
|
+
error=>{
|
|
206
|
+
this.#reportImportedDestroyError(error,reportContext);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
.finally(()=>this.#restoreImportedDestroy(record));
|
|
54
211
|
}
|
|
55
212
|
|
|
56
|
-
async #loadHTML(href,resolvedHref,response){
|
|
213
|
+
async #loadHTML(href,resolvedHref,response,generation,controller,operationId){
|
|
57
214
|
if (!response.ok) {
|
|
58
215
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
59
216
|
}
|
|
60
217
|
|
|
61
218
|
const html = await response.text();
|
|
219
|
+
if(!this.#isCurrentConnection(generation,controller))return false;
|
|
220
|
+
await this.#destroyImportedHost();
|
|
221
|
+
if(!this.#isCurrentConnection(generation,controller))return false;
|
|
62
222
|
this.shadowRoot.innerHTML = html;
|
|
63
223
|
|
|
64
224
|
await this.#executeScripts();
|
|
225
|
+
if(!this.#isCurrentConnection(generation,controller)){
|
|
226
|
+
await this.#destroyImportedHost();
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
65
229
|
|
|
66
230
|
this.ready=true;
|
|
67
|
-
this.
|
|
231
|
+
const {occurrence}=this.#events.dispatch(
|
|
232
|
+
'html-import-ready',
|
|
233
|
+
Object.freeze({href,resolvedHref}),
|
|
234
|
+
{operationId,publicDetail:Object.freeze({ready:true})}
|
|
235
|
+
);
|
|
236
|
+
projectArcaneDOMEvent(this,occurrence,{
|
|
68
237
|
bubbles:true,
|
|
69
|
-
composed:true
|
|
70
|
-
|
|
71
|
-
}));
|
|
238
|
+
composed:true
|
|
239
|
+
});
|
|
72
240
|
return true;
|
|
73
241
|
}
|
|
74
242
|
|
|
75
243
|
async #executeScripts() {
|
|
76
244
|
const scripts = Array.from(this.shadowRoot.querySelectorAll('script'));
|
|
77
|
-
|
|
245
|
+
const previousDescriptor=Object.getOwnPropertyDescriptor(this,'destroy')??null;
|
|
246
|
+
const previousDestroy=this.destroy;
|
|
247
|
+
try{
|
|
248
|
+
for(const script of scripts){
|
|
78
249
|
if (script.src) {
|
|
79
250
|
console.error('ONLY INLINE SCRIPTS SUPPORTED AT THIS TIME FOR SECURITY REASONS');
|
|
80
251
|
console.warn('script src path will need to be limited to ./{text}.js, ../{text}.js), or acceptable sub folders. This can be complex.');
|
|
@@ -105,6 +276,19 @@ class HTMLImport extends HTMLElement {
|
|
|
105
276
|
htmlImportHostRegistry.delete(hostToken);
|
|
106
277
|
delete executable.dataset.arcaneHostToken;
|
|
107
278
|
}
|
|
279
|
+
}
|
|
280
|
+
}finally{
|
|
281
|
+
const installedDescriptor=Object.getOwnPropertyDescriptor(this,'destroy')??null;
|
|
282
|
+
const installedDestroy=this.destroy;
|
|
283
|
+
if(typeof installedDestroy==='function'
|
|
284
|
+
&&installedDestroy!==previousDestroy
|
|
285
|
+
&&!samePropertyDescriptor(installedDescriptor,previousDescriptor)){
|
|
286
|
+
this.#importedDestroy={
|
|
287
|
+
destroy:installedDestroy,
|
|
288
|
+
installedDescriptor,
|
|
289
|
+
previousDescriptor
|
|
290
|
+
};
|
|
291
|
+
}
|
|
108
292
|
}
|
|
109
293
|
}
|
|
110
294
|
}
|
|
@@ -2,9 +2,28 @@ import {
|
|
|
2
2
|
checkLocalAIReadiness,
|
|
3
3
|
deriveLocalAIRequirements
|
|
4
4
|
} from './LocalAIReadiness.js?v=4';
|
|
5
|
+
import {
|
|
6
|
+
createArcaneEventSource,
|
|
7
|
+
projectArcaneDOMEvent
|
|
8
|
+
} from 'arcane-os/event-manager';
|
|
5
9
|
|
|
6
10
|
const SLOT_NAMES=Object.freeze(['llm','stt','tts']);
|
|
7
11
|
|
|
12
|
+
export const LOCAL_AI_READINESS_CONTROLLER_EVENT_TYPES=Object.freeze({
|
|
13
|
+
changed:'local-ai-readiness-change'
|
|
14
|
+
});
|
|
15
|
+
export const LOCAL_AI_READINESS_CONTROLLER_ERROR_CODES=Object.freeze({
|
|
16
|
+
aborted:'ARCANE_LOCAL_AI_READINESS_CONTROLLER_ABORTED',
|
|
17
|
+
disposed:'ARCANE_LOCAL_AI_READINESS_CONTROLLER_DISPOSED',
|
|
18
|
+
statusReadyTimeout:'ARCANE_LOCAL_AI_STATUS_COMPONENT_READY_TIMEOUT'
|
|
19
|
+
});
|
|
20
|
+
export const LOCAL_AI_READINESS_CONTROLLER_REASONS=Object.freeze({
|
|
21
|
+
checked:'local-ai-readiness-checked',
|
|
22
|
+
aborted:'local-ai-readiness-controller-aborted',
|
|
23
|
+
disposed:'local-ai-readiness-controller-disposed',
|
|
24
|
+
statusReadyTimeout:'local-ai-status-component-ready-timeout'
|
|
25
|
+
});
|
|
26
|
+
|
|
8
27
|
function availabilityFromReport(report={}){
|
|
9
28
|
const slots=report.slots||{};
|
|
10
29
|
return Object.freeze(Object.fromEntries(SLOT_NAMES.map(name=>{
|
|
@@ -29,19 +48,85 @@ function pendingReport(requirements){
|
|
|
29
48
|
});
|
|
30
49
|
}
|
|
31
50
|
|
|
32
|
-
|
|
51
|
+
function localAIReadinessAbortError(reason,disposed=false){
|
|
52
|
+
const code=disposed
|
|
53
|
+
?LOCAL_AI_READINESS_CONTROLLER_ERROR_CODES.disposed
|
|
54
|
+
:LOCAL_AI_READINESS_CONTROLLER_ERROR_CODES.aborted;
|
|
55
|
+
const errorReason=disposed
|
|
56
|
+
?LOCAL_AI_READINESS_CONTROLLER_REASONS.disposed
|
|
57
|
+
:LOCAL_AI_READINESS_CONTROLLER_REASONS.aborted;
|
|
58
|
+
if(reason instanceof Error
|
|
59
|
+
&&reason.name==='AbortError'
|
|
60
|
+
&&reason.code===code
|
|
61
|
+
&&reason.reason===errorReason){
|
|
62
|
+
return reason;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const error=new Error(
|
|
66
|
+
disposed
|
|
67
|
+
?'The local AI readiness controller has been disposed.'
|
|
68
|
+
:'The local AI readiness check was aborted.'
|
|
69
|
+
);
|
|
70
|
+
error.name='AbortError';
|
|
71
|
+
error.code=code;
|
|
72
|
+
error.reason=errorReason;
|
|
73
|
+
if(reason!==undefined){
|
|
74
|
+
error.cause=reason;
|
|
75
|
+
}
|
|
76
|
+
return error;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function waitForStatusComponent(status,signal){
|
|
33
80
|
if(!status||status.ready){
|
|
34
81
|
return status;
|
|
35
82
|
}
|
|
36
83
|
|
|
37
|
-
await new Promise(resolve
|
|
38
|
-
|
|
39
|
-
|
|
84
|
+
await new Promise(function waitForLocalAIStatusPromise(resolve,reject){
|
|
85
|
+
let settled=false;
|
|
86
|
+
const timeout=globalThis.setTimeout(failTimeout,10000);
|
|
87
|
+
|
|
88
|
+
function cleanup(){
|
|
40
89
|
globalThis.clearTimeout(timeout);
|
|
41
|
-
status.removeEventListener('local-ai-status-ready',
|
|
90
|
+
status.removeEventListener('local-ai-status-ready',complete);
|
|
91
|
+
signal?.removeEventListener('abort',abortWait);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function complete(){
|
|
95
|
+
if(settled){
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
settled=true;
|
|
99
|
+
cleanup();
|
|
42
100
|
resolve();
|
|
43
101
|
}
|
|
44
|
-
|
|
102
|
+
|
|
103
|
+
function fail(error){
|
|
104
|
+
if(settled){
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
settled=true;
|
|
108
|
+
cleanup();
|
|
109
|
+
reject(error);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function failTimeout(){
|
|
113
|
+
const error=new Error('Local AI status component readiness timed out after 10000 ms.');
|
|
114
|
+
error.code=LOCAL_AI_READINESS_CONTROLLER_ERROR_CODES.statusReadyTimeout;
|
|
115
|
+
error.reason=LOCAL_AI_READINESS_CONTROLLER_REASONS.statusReadyTimeout;
|
|
116
|
+
fail(error);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function abortWait(){
|
|
120
|
+
fail(localAIReadinessAbortError(signal?.reason));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if(signal?.aborted){
|
|
124
|
+
abortWait();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
signal?.addEventListener('abort',abortWait,{once:true});
|
|
129
|
+
status.addEventListener('local-ai-status-ready',complete,{once:true});
|
|
45
130
|
});
|
|
46
131
|
return status;
|
|
47
132
|
}
|
|
@@ -62,35 +147,76 @@ export function createLocalAIReadinessController({
|
|
|
62
147
|
throw new TypeError('A chat component is required.');
|
|
63
148
|
}
|
|
64
149
|
|
|
150
|
+
const lifecycleController=new AbortController();
|
|
151
|
+
const eventOwner=Object.freeze({kind:'local-ai-readiness-controller'});
|
|
152
|
+
const events=createArcaneEventSource(
|
|
153
|
+
eventOwner,
|
|
154
|
+
{
|
|
155
|
+
source:'local-ai-readiness-controller',
|
|
156
|
+
eventTypes:Object.freeze(
|
|
157
|
+
Object.values(LOCAL_AI_READINESS_CONTROLLER_EVENT_TYPES)
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
);
|
|
65
161
|
let active=null;
|
|
162
|
+
let chatReadyListenerPending=false;
|
|
163
|
+
let destroyed=false;
|
|
66
164
|
let latestReport=null;
|
|
67
165
|
let currentAvailability=availabilityFromReport({});
|
|
166
|
+
let operationSequence=0;
|
|
68
167
|
|
|
69
|
-
|
|
168
|
+
function publishAvailability(){
|
|
169
|
+
if(destroyed){
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
70
172
|
if(typeof chat.setAIAvailability==='function'){
|
|
173
|
+
if(chatReadyListenerPending){
|
|
174
|
+
chat.removeEventListener('chat-ready',publishAvailability);
|
|
175
|
+
}
|
|
176
|
+
chatReadyListenerPending=false;
|
|
71
177
|
chat.setAIAvailability(currentAvailability);
|
|
72
|
-
}else{
|
|
73
|
-
|
|
178
|
+
}else if(!chatReadyListenerPending){
|
|
179
|
+
chatReadyListenerPending=true;
|
|
180
|
+
chat.addEventListener(
|
|
181
|
+
'chat-ready',
|
|
182
|
+
publishAvailability,
|
|
183
|
+
{
|
|
184
|
+
once:true,
|
|
185
|
+
signal:lifecycleController.signal
|
|
186
|
+
}
|
|
187
|
+
);
|
|
74
188
|
}
|
|
75
|
-
}
|
|
189
|
+
}
|
|
76
190
|
|
|
77
|
-
|
|
191
|
+
function applyAvailability(report){
|
|
78
192
|
currentAvailability=availabilityFromReport(report);
|
|
79
193
|
publishAvailability();
|
|
80
|
-
}
|
|
194
|
+
}
|
|
81
195
|
|
|
82
|
-
|
|
196
|
+
async function check(){
|
|
197
|
+
if(destroyed){
|
|
198
|
+
throw localAIReadinessAbortError(
|
|
199
|
+
lifecycleController.signal.reason,
|
|
200
|
+
true
|
|
201
|
+
);
|
|
202
|
+
}
|
|
83
203
|
if(active){
|
|
84
204
|
return active;
|
|
85
205
|
}
|
|
86
206
|
|
|
87
|
-
active=(async()
|
|
207
|
+
active=(async function runLocalAIReadinessCheck(){
|
|
88
208
|
const preferenceTuple=selectedPreferences(preferences);
|
|
89
209
|
const requirements=deriveLocalAIRequirements(preferenceTuple);
|
|
90
210
|
const required=SLOT_NAMES.some(name=>requirements[name].required);
|
|
91
211
|
|
|
92
212
|
applyAvailability(pendingReport(requirements));
|
|
93
|
-
await waitForStatusComponent(status);
|
|
213
|
+
await waitForStatusComponent(status,lifecycleController.signal);
|
|
214
|
+
if(destroyed){
|
|
215
|
+
throw localAIReadinessAbortError(
|
|
216
|
+
lifecycleController.signal.reason,
|
|
217
|
+
true
|
|
218
|
+
);
|
|
219
|
+
}
|
|
94
220
|
status?.configure?.({profileHref});
|
|
95
221
|
if(required){
|
|
96
222
|
status?.begin?.(requirements);
|
|
@@ -102,42 +228,95 @@ export function createLocalAIReadinessController({
|
|
|
102
228
|
...checkOptions,
|
|
103
229
|
preferences:preferenceTuple
|
|
104
230
|
});
|
|
231
|
+
if(destroyed){
|
|
232
|
+
throw localAIReadinessAbortError(
|
|
233
|
+
lifecycleController.signal.reason,
|
|
234
|
+
true
|
|
235
|
+
);
|
|
236
|
+
}
|
|
105
237
|
status?.present?.(latestReport);
|
|
106
238
|
applyAvailability(latestReport);
|
|
107
239
|
if(typeof onChange==='function'){
|
|
108
240
|
onChange(latestReport);
|
|
109
241
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
242
|
+
const availability=availabilityFromReport(latestReport);
|
|
243
|
+
if(destroyed){
|
|
244
|
+
throw localAIReadinessAbortError(
|
|
245
|
+
lifecycleController.signal.reason,
|
|
246
|
+
true
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
operationSequence+=1;
|
|
250
|
+
const operationId=`${events.instanceId}:check:${operationSequence.toString(36)}`;
|
|
251
|
+
const reason=LOCAL_AI_READINESS_CONTROLLER_REASONS.checked;
|
|
252
|
+
const publication=events.dispatch(
|
|
253
|
+
LOCAL_AI_READINESS_CONTROLLER_EVENT_TYPES.changed,
|
|
254
|
+
Object.freeze({operationId,reason,report:latestReport}),
|
|
255
|
+
{
|
|
256
|
+
operationId,
|
|
257
|
+
publicDetail:Object.freeze({availability,reason})
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
projectArcaneDOMEvent(
|
|
261
|
+
chat,
|
|
262
|
+
publication.occurrence,
|
|
263
|
+
{
|
|
264
|
+
bubbles:true,
|
|
265
|
+
composed:true
|
|
266
|
+
}
|
|
267
|
+
);
|
|
115
268
|
return latestReport;
|
|
116
|
-
})().finally(()
|
|
269
|
+
})().finally(function releaseLocalAIReadinessCheck(){
|
|
117
270
|
active=null;
|
|
118
271
|
});
|
|
119
272
|
|
|
120
273
|
return active;
|
|
121
|
-
}
|
|
274
|
+
}
|
|
122
275
|
|
|
123
|
-
|
|
276
|
+
function ensure(){
|
|
124
277
|
if(active){
|
|
125
278
|
return active;
|
|
126
279
|
}
|
|
127
280
|
return latestReport
|
|
128
281
|
?Promise.resolve(latestReport)
|
|
129
282
|
:check();
|
|
130
|
-
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function retry(){
|
|
286
|
+
void check().catch(function ignoreDestroyedLocalAIRetry(error){
|
|
287
|
+
if(error?.name!=='AbortError'){
|
|
288
|
+
console.error('Local AI readiness retry failed.',error);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
status?.addEventListener(
|
|
293
|
+
'local-ai-retry',
|
|
294
|
+
retry,
|
|
295
|
+
{signal:lifecycleController.signal}
|
|
296
|
+
);
|
|
131
297
|
|
|
132
|
-
|
|
133
|
-
|
|
298
|
+
function destroy(){
|
|
299
|
+
if(destroyed){
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
destroyed=true;
|
|
303
|
+
lifecycleController.abort(
|
|
304
|
+
localAIReadinessAbortError(undefined,true)
|
|
305
|
+
);
|
|
306
|
+
if(chatReadyListenerPending){
|
|
307
|
+
chat.removeEventListener('chat-ready',publishAvailability);
|
|
308
|
+
chatReadyListenerPending=false;
|
|
309
|
+
}
|
|
310
|
+
status?.removeEventListener('local-ai-retry',retry);
|
|
311
|
+
events.dispose();
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
134
314
|
|
|
135
315
|
return Object.freeze({
|
|
136
316
|
check,
|
|
137
317
|
ensure,
|
|
138
|
-
destroy
|
|
139
|
-
|
|
140
|
-
},
|
|
318
|
+
destroy,
|
|
319
|
+
dispose:destroy,
|
|
141
320
|
get availability(){
|
|
142
321
|
return currentAvailability;
|
|
143
322
|
},
|