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,5 @@
|
|
|
1
|
+
import {createArcaneEventSource} from 'arcane-os/event-manager';
|
|
2
|
+
|
|
1
3
|
const DEFAULT_MAX_TESTS=64;
|
|
2
4
|
const HARD_MAX_TESTS=256;
|
|
3
5
|
const DEFAULT_TIMEOUT_MS=5000;
|
|
@@ -7,6 +9,45 @@ const CONTROL_CHARACTERS=/[\u0000-\u001f\u007f]/;
|
|
|
7
9
|
const TEST_ID_PATTERN=/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
8
10
|
const RESULT_STATUSES=new Set(['fail','pass','skip']);
|
|
9
11
|
|
|
12
|
+
export const BROWSER_TEST_SUITE_EVENT_TYPES=Object.freeze({
|
|
13
|
+
runCompleted:'browser-test-suite-complete',
|
|
14
|
+
runStarted:'browser-test-suite-start',
|
|
15
|
+
testCompleted:'browser-test-result',
|
|
16
|
+
testStarted:'browser-test-start'
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const BROWSER_TEST_SUITE_ERROR_CODES=Object.freeze({
|
|
20
|
+
assertion:'BROWSER_TEST_ASSERTION',
|
|
21
|
+
busy:'BROWSER_TEST_BUSY',
|
|
22
|
+
callbackRejectedLegacy:'BROWSER_TEST_ERROR',
|
|
23
|
+
clockInvalid:'BROWSER_TEST_INVALID_CLOCK',
|
|
24
|
+
descriptorCaseCollision:'BROWSER_TEST_CASE_COLLISION',
|
|
25
|
+
descriptorInvalid:'BROWSER_TEST_INVALID_DESCRIPTOR',
|
|
26
|
+
disposed:'BROWSER_TEST_SUITE_DISPOSED',
|
|
27
|
+
limitExceeded:'BROWSER_TEST_LIMIT',
|
|
28
|
+
limitInvalid:'BROWSER_TEST_INVALID_LIMIT',
|
|
29
|
+
optionsInvalid:'BROWSER_TEST_INVALID_OPTIONS',
|
|
30
|
+
resultInvalid:'BROWSER_TEST_INVALID_RESULT',
|
|
31
|
+
runAborted:'BROWSER_TEST_ABORTED',
|
|
32
|
+
skipped:'BROWSER_TEST_SKIP',
|
|
33
|
+
timedOut:'BROWSER_TEST_TIMEOUT'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const BROWSER_TEST_SUITE_REASONS=Object.freeze({
|
|
37
|
+
runAborted:'browser-test-run-aborted',
|
|
38
|
+
runCompleted:'browser-test-run-completed',
|
|
39
|
+
runFailed:'browser-test-run-failed',
|
|
40
|
+
runStarted:'browser-test-run-started',
|
|
41
|
+
runWithoutPassesCompleted:'browser-test-run-without-passes-completed',
|
|
42
|
+
suiteDisposed:'browser-test-suite-disposed',
|
|
43
|
+
testCallbackRejected:'browser-test-callback-rejected',
|
|
44
|
+
testFailed:'browser-test-failed',
|
|
45
|
+
testPassed:'browser-test-passed',
|
|
46
|
+
testSkipped:'browser-test-skipped',
|
|
47
|
+
testStarted:'browser-test-started',
|
|
48
|
+
testTimedOut:'browser-test-timed-out'
|
|
49
|
+
});
|
|
50
|
+
|
|
10
51
|
function isPlainRecord(value){
|
|
11
52
|
return Boolean(value)
|
|
12
53
|
&&typeof value==='object'
|
|
@@ -107,13 +148,6 @@ function normalizeRunOptions(input){
|
|
|
107
148
|
return Object.freeze({context:input.context,signal});
|
|
108
149
|
}
|
|
109
150
|
|
|
110
|
-
function detailEvent(type,detail){
|
|
111
|
-
if(typeof globalThis.CustomEvent==='function') return new CustomEvent(type,{detail});
|
|
112
|
-
const event=new Event(type);
|
|
113
|
-
Object.defineProperty(event,'detail',{enumerable:true,value:detail});
|
|
114
|
-
return event;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
151
|
function elapsed(now,start){
|
|
118
152
|
const end=Number(now());
|
|
119
153
|
if(!Number.isFinite(end)) fail('now() must return a finite number.','BROWSER_TEST_INVALID_CLOCK');
|
|
@@ -138,20 +172,26 @@ function assertionError(message){
|
|
|
138
172
|
return error;
|
|
139
173
|
}
|
|
140
174
|
|
|
141
|
-
function abortError(){
|
|
142
|
-
const error=coded(
|
|
175
|
+
function abortError(cause){
|
|
176
|
+
const error=coded(
|
|
177
|
+
new Error('The browser test run was aborted.'),
|
|
178
|
+
BROWSER_TEST_SUITE_ERROR_CODES.runAborted
|
|
179
|
+
);
|
|
143
180
|
error.name='AbortError';
|
|
181
|
+
error.reason=BROWSER_TEST_SUITE_REASONS.runAborted;
|
|
182
|
+
if(cause!==undefined)error.cause=cause;
|
|
144
183
|
return error;
|
|
145
184
|
}
|
|
146
185
|
|
|
147
186
|
function timeoutError(milliseconds){
|
|
148
187
|
const error=coded(new Error(`The check exceeded ${milliseconds} milliseconds.`),'BROWSER_TEST_TIMEOUT');
|
|
149
188
|
error.name='BrowserTestTimeoutError';
|
|
189
|
+
error.reason=BROWSER_TEST_SUITE_REASONS.testTimedOut;
|
|
150
190
|
return error;
|
|
151
191
|
}
|
|
152
192
|
|
|
153
193
|
function runWithTimeout(callback,{signal,timeoutMs}){
|
|
154
|
-
if(signal?.aborted) return Promise.reject(abortError());
|
|
194
|
+
if(signal?.aborted) return Promise.reject(abortError(signal.reason));
|
|
155
195
|
const controller=new AbortController();
|
|
156
196
|
return new Promise((resolve,reject)=>{
|
|
157
197
|
let settled=false;
|
|
@@ -163,8 +203,8 @@ function runWithTimeout(callback,{signal,timeoutMs}){
|
|
|
163
203
|
handler(value);
|
|
164
204
|
};
|
|
165
205
|
const onAbort=()=>{
|
|
166
|
-
controller.abort();
|
|
167
|
-
finish(reject,abortError());
|
|
206
|
+
controller.abort(signal.reason);
|
|
207
|
+
finish(reject,abortError(signal.reason));
|
|
168
208
|
};
|
|
169
209
|
const timer=setTimeout(()=>{
|
|
170
210
|
controller.abort();
|
|
@@ -199,7 +239,7 @@ function outcomeFromError(error){
|
|
|
199
239
|
if(error?.code==='BROWSER_TEST_SKIP'){
|
|
200
240
|
return Object.freeze({status:'skip',message:resultMessage(error.message,'Skipped.'),code:error.code});
|
|
201
241
|
}
|
|
202
|
-
if(error?.code===
|
|
242
|
+
if(error?.code===BROWSER_TEST_SUITE_ERROR_CODES.runAborted){
|
|
203
243
|
return Object.freeze({status:'skip',message:resultMessage(error.message,'The run was aborted.'),code:error.code});
|
|
204
244
|
}
|
|
205
245
|
return Object.freeze({
|
|
@@ -226,6 +266,79 @@ function skippedResult(test,message,code='BROWSER_TEST_ABORTED'){
|
|
|
226
266
|
return resultRecord(test,{status:'skip',message,code},0);
|
|
227
267
|
}
|
|
228
268
|
|
|
269
|
+
function browserTestResultReason(result){
|
|
270
|
+
if(result.code===BROWSER_TEST_SUITE_ERROR_CODES.runAborted){
|
|
271
|
+
return BROWSER_TEST_SUITE_REASONS.runAborted;
|
|
272
|
+
}
|
|
273
|
+
if(result.code===BROWSER_TEST_SUITE_ERROR_CODES.timedOut){
|
|
274
|
+
return BROWSER_TEST_SUITE_REASONS.testTimedOut;
|
|
275
|
+
}
|
|
276
|
+
if(result.code===BROWSER_TEST_SUITE_ERROR_CODES.callbackRejectedLegacy){
|
|
277
|
+
return BROWSER_TEST_SUITE_REASONS.testCallbackRejected;
|
|
278
|
+
}
|
|
279
|
+
if(result.status==='pass')return BROWSER_TEST_SUITE_REASONS.testPassed;
|
|
280
|
+
if(result.status==='skip')return BROWSER_TEST_SUITE_REASONS.testSkipped;
|
|
281
|
+
return BROWSER_TEST_SUITE_REASONS.testFailed;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function browserTestPublicDetail(type,detail){
|
|
285
|
+
const result=detail?.result;
|
|
286
|
+
const test=detail?.test;
|
|
287
|
+
const testCount=Number.isSafeInteger(detail?.total)
|
|
288
|
+
?detail.total
|
|
289
|
+
:Number.isSafeInteger(detail?.totals?.total)
|
|
290
|
+
?detail.totals.total
|
|
291
|
+
:null;
|
|
292
|
+
if(type===BROWSER_TEST_SUITE_EVENT_TYPES.runStarted){
|
|
293
|
+
return Object.freeze({
|
|
294
|
+
reason:BROWSER_TEST_SUITE_REASONS.runStarted,
|
|
295
|
+
testCount
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if(type===BROWSER_TEST_SUITE_EVENT_TYPES.testStarted){
|
|
299
|
+
return Object.freeze({
|
|
300
|
+
reason:BROWSER_TEST_SUITE_REASONS.testStarted,
|
|
301
|
+
testId:test.id,
|
|
302
|
+
testIndex:detail.index,
|
|
303
|
+
testCount
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if(type===BROWSER_TEST_SUITE_EVENT_TYPES.testCompleted){
|
|
307
|
+
return Object.freeze({
|
|
308
|
+
reason:browserTestResultReason(result),
|
|
309
|
+
testId:result.id,
|
|
310
|
+
testIndex:detail.index,
|
|
311
|
+
testCount,
|
|
312
|
+
status:result.status,
|
|
313
|
+
...(typeof result.code==='string'?{code:result.code}:{})
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const reason=detail.status==='aborted'
|
|
317
|
+
?BROWSER_TEST_SUITE_REASONS.runAborted
|
|
318
|
+
:detail.status==='fail'
|
|
319
|
+
?BROWSER_TEST_SUITE_REASONS.runFailed
|
|
320
|
+
:detail.status==='skip'
|
|
321
|
+
?BROWSER_TEST_SUITE_REASONS.runWithoutPassesCompleted
|
|
322
|
+
:BROWSER_TEST_SUITE_REASONS.runCompleted;
|
|
323
|
+
return Object.freeze({
|
|
324
|
+
reason,
|
|
325
|
+
status:detail.status,
|
|
326
|
+
testCount,
|
|
327
|
+
passedTestCount:detail.totals.pass,
|
|
328
|
+
failedTestCount:detail.totals.fail,
|
|
329
|
+
skippedTestCount:detail.totals.skip
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function disposedError(){
|
|
334
|
+
const error=coded(
|
|
335
|
+
new Error('The browser test suite has been disposed.'),
|
|
336
|
+
BROWSER_TEST_SUITE_ERROR_CODES.disposed
|
|
337
|
+
);
|
|
338
|
+
error.reason=BROWSER_TEST_SUITE_REASONS.suiteDisposed;
|
|
339
|
+
return error;
|
|
340
|
+
}
|
|
341
|
+
|
|
229
342
|
/**
|
|
230
343
|
* Runs a fixed inventory of parent-supplied browser checks sequentially.
|
|
231
344
|
*
|
|
@@ -236,7 +349,11 @@ function skippedResult(test,message,code='BROWSER_TEST_ABORTED'){
|
|
|
236
349
|
* the page and stop work they started after their abort signal fires.
|
|
237
350
|
*/
|
|
238
351
|
export default class BrowserTestSuite extends EventTarget{
|
|
352
|
+
#activeRun=null;
|
|
353
|
+
#disposed=false;
|
|
354
|
+
#events;
|
|
239
355
|
#now;
|
|
356
|
+
#operationSequence=0;
|
|
240
357
|
#running=false;
|
|
241
358
|
#tests;
|
|
242
359
|
|
|
@@ -245,8 +362,17 @@ export default class BrowserTestSuite extends EventTarget{
|
|
|
245
362
|
const normalized=normalizeOptions(options);
|
|
246
363
|
this.#now=normalized.now;
|
|
247
364
|
this.#tests=normalized.tests;
|
|
365
|
+
this.#events=createArcaneEventSource(this,{
|
|
366
|
+
source:'browser-test-suite',
|
|
367
|
+
eventTypes:Object.freeze(Object.values(BROWSER_TEST_SUITE_EVENT_TYPES))
|
|
368
|
+
});
|
|
248
369
|
}
|
|
249
370
|
|
|
371
|
+
addEventListener(type,listener,options){return this.#events.addEventListener(type,listener,options);}
|
|
372
|
+
removeEventListener(type,listener,options){return this.#events.removeEventListener(type,listener,options);}
|
|
373
|
+
on(type,listener,options){return this.#events.on(type,listener,options);}
|
|
374
|
+
dispatchEvent(value){return this.#events.dispatchEvent(value);}
|
|
375
|
+
|
|
250
376
|
get running(){return this.#running;}
|
|
251
377
|
|
|
252
378
|
list(){
|
|
@@ -257,33 +383,67 @@ export default class BrowserTestSuite extends EventTarget{
|
|
|
257
383
|
})));
|
|
258
384
|
}
|
|
259
385
|
|
|
260
|
-
#emit(type,detail){
|
|
261
|
-
this
|
|
386
|
+
#emit(type,detail,operationId){
|
|
387
|
+
if(this.#disposed)return false;
|
|
388
|
+
const compatibilityDetail=Object.freeze(detail);
|
|
389
|
+
return this.#events.dispatch(type,compatibilityDetail,{
|
|
390
|
+
operationId,
|
|
391
|
+
publicDetail:browserTestPublicDetail(type,compatibilityDetail)
|
|
392
|
+
});
|
|
262
393
|
}
|
|
263
394
|
|
|
264
395
|
async run(options={}){
|
|
396
|
+
if(this.#disposed)throw disposedError();
|
|
265
397
|
if(this.#running) fail('A browser test run is already active.','BROWSER_TEST_BUSY',Error);
|
|
266
398
|
const settings=normalizeRunOptions(options);
|
|
267
399
|
const suiteStart=startTime(this.#now);
|
|
400
|
+
const controller=new AbortController();
|
|
401
|
+
const operationId=`${this.#events.instanceId}:run:${(++this.#operationSequence).toString(36)}`;
|
|
402
|
+
let removeAbortListener=null;
|
|
403
|
+
function abortBrowserTestRun(){
|
|
404
|
+
if(!controller.signal.aborted)controller.abort(settings.signal?.reason);
|
|
405
|
+
}
|
|
406
|
+
if(settings.signal){
|
|
407
|
+
settings.signal.addEventListener('abort',abortBrowserTestRun,{once:true});
|
|
408
|
+
removeAbortListener=function removeBrowserTestRunAbortListener(){
|
|
409
|
+
settings.signal.removeEventListener('abort',abortBrowserTestRun);
|
|
410
|
+
};
|
|
411
|
+
if(settings.signal.aborted)abortBrowserTestRun();
|
|
412
|
+
}
|
|
413
|
+
const runRecord=Object.freeze({controller,operationId});
|
|
414
|
+
this.#activeRun=runRecord;
|
|
268
415
|
const results=[];
|
|
269
|
-
let aborted=
|
|
416
|
+
let aborted=controller.signal.aborted;
|
|
270
417
|
this.#running=true;
|
|
271
|
-
this.#emit('browser-test-suite-start',{tests:this.list(),total:this.#tests.length});
|
|
272
418
|
try{
|
|
419
|
+
this.#emit(
|
|
420
|
+
BROWSER_TEST_SUITE_EVENT_TYPES.runStarted,
|
|
421
|
+
{tests:this.list(),total:this.#tests.length},
|
|
422
|
+
operationId
|
|
423
|
+
);
|
|
273
424
|
for(let index=0;index<this.#tests.length;index++){
|
|
274
425
|
const descriptor=this.#tests[index];
|
|
275
|
-
|
|
426
|
+
const testOperationId=`${operationId}:test:${(index+1).toString(36)}`;
|
|
427
|
+
if(aborted||controller.signal.aborted){
|
|
276
428
|
aborted=true;
|
|
277
429
|
const result=skippedResult(descriptor,'Skipped because the browser test run was aborted.');
|
|
278
430
|
results.push(result);
|
|
279
|
-
this.#emit(
|
|
431
|
+
this.#emit(
|
|
432
|
+
BROWSER_TEST_SUITE_EVENT_TYPES.testCompleted,
|
|
433
|
+
{index,result,total:this.#tests.length},
|
|
434
|
+
testOperationId
|
|
435
|
+
);
|
|
280
436
|
continue;
|
|
281
437
|
}
|
|
282
|
-
this.#emit(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
438
|
+
this.#emit(
|
|
439
|
+
BROWSER_TEST_SUITE_EVENT_TYPES.testStarted,
|
|
440
|
+
{
|
|
441
|
+
index,
|
|
442
|
+
test:Object.freeze({id:descriptor.id,name:descriptor.name,timeoutMs:descriptor.timeoutMs}),
|
|
443
|
+
total:this.#tests.length,
|
|
444
|
+
},
|
|
445
|
+
testOperationId
|
|
446
|
+
);
|
|
287
447
|
const testStart=startTime(this.#now);
|
|
288
448
|
let outcome;
|
|
289
449
|
try{
|
|
@@ -292,18 +452,24 @@ export default class BrowserTestSuite extends EventTarget{
|
|
|
292
452
|
context:settings.context,
|
|
293
453
|
signal,
|
|
294
454
|
skip(message){throw skipError(message);},
|
|
295
|
-
})),{signal:
|
|
455
|
+
})),{signal:controller.signal,timeoutMs:descriptor.timeoutMs});
|
|
296
456
|
outcome=normalizedOutcome(value);
|
|
297
457
|
}catch(error){
|
|
298
458
|
outcome=outcomeFromError(error);
|
|
299
|
-
if(error?.code===
|
|
459
|
+
if(error?.code===BROWSER_TEST_SUITE_ERROR_CODES.runAborted) aborted=true;
|
|
300
460
|
}
|
|
301
461
|
const result=resultRecord(descriptor,outcome,elapsed(this.#now,testStart));
|
|
302
462
|
results.push(result);
|
|
303
|
-
this.#emit(
|
|
463
|
+
this.#emit(
|
|
464
|
+
BROWSER_TEST_SUITE_EVENT_TYPES.testCompleted,
|
|
465
|
+
{index,result,total:this.#tests.length},
|
|
466
|
+
testOperationId
|
|
467
|
+
);
|
|
304
468
|
}
|
|
305
469
|
}finally{
|
|
306
470
|
this.#running=false;
|
|
471
|
+
removeAbortListener?.();
|
|
472
|
+
if(this.#activeRun===runRecord)this.#activeRun=null;
|
|
307
473
|
}
|
|
308
474
|
const totals=Object.freeze({
|
|
309
475
|
total:results.length,
|
|
@@ -318,9 +484,20 @@ export default class BrowserTestSuite extends EventTarget{
|
|
|
318
484
|
durationMs:elapsed(this.#now,suiteStart),
|
|
319
485
|
results:Object.freeze(results),
|
|
320
486
|
});
|
|
321
|
-
this.#emit(
|
|
487
|
+
this.#emit(BROWSER_TEST_SUITE_EVENT_TYPES.runCompleted,summary,operationId);
|
|
322
488
|
return summary;
|
|
323
489
|
}
|
|
490
|
+
|
|
491
|
+
dispose(){
|
|
492
|
+
if(this.#disposed)return false;
|
|
493
|
+
this.#disposed=true;
|
|
494
|
+
if(this.#activeRun&&!this.#activeRun.controller.signal.aborted){
|
|
495
|
+
this.#activeRun.controller.abort(disposedError());
|
|
496
|
+
}
|
|
497
|
+
this.#events.dispose();
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
destroy(){return this.dispose();}
|
|
324
501
|
}
|
|
325
502
|
|
|
326
503
|
export {assertionError,skipError};
|
|
@@ -1,7 +1,38 @@
|
|
|
1
|
+
import {createArcaneEventSource} from 'arcane-os/event-manager';
|
|
1
2
|
import Calculation from '../entities/Calculation.js';
|
|
2
3
|
|
|
3
4
|
const FUNCTIONS=Object.freeze({sqrt:Math.sqrt,abs:Math.abs,sin:Math.sin,cos:Math.cos,tan:Math.tan,log:Math.log10,ln:Math.log});
|
|
4
5
|
const CONSTANTS=Object.freeze({pi:Math.PI,e:Math.E});
|
|
6
|
+
export const CALCULATOR_ENGINE_ERROR_CODES=Object.freeze({
|
|
7
|
+
disposed:'ARCANE_CALCULATOR_ENGINE_DISPOSED',
|
|
8
|
+
domain:'ARCANE_CALCULATOR_EXPRESSION_DOMAIN_INVALID',
|
|
9
|
+
evaluation:'ARCANE_CALCULATOR_EXPRESSION_EVALUATION_FAILED',
|
|
10
|
+
input:'ARCANE_CALCULATOR_EXPRESSION_INPUT_INVALID',
|
|
11
|
+
syntax:'ARCANE_CALCULATOR_EXPRESSION_SYNTAX_INVALID'
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function calculatorErrorCode(error){
|
|
15
|
+
if(error instanceof TypeError)return CALCULATOR_ENGINE_ERROR_CODES.input;
|
|
16
|
+
if(error instanceof SyntaxError)return CALCULATOR_ENGINE_ERROR_CODES.syntax;
|
|
17
|
+
if(error instanceof RangeError)return CALCULATOR_ENGINE_ERROR_CODES.domain;
|
|
18
|
+
return CALCULATOR_ENGINE_ERROR_CODES.evaluation;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function attachCalculatorErrorCode(error,code){
|
|
22
|
+
if(!error||(typeof error!=='object'&&typeof error!=='function'))return error;
|
|
23
|
+
try{Object.defineProperty(error,'code',{configurable:true,enumerable:false,value:code,writable:true});}
|
|
24
|
+
catch{try{error.code=code;}catch{}}
|
|
25
|
+
return error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compatibilityExpression(value){try{return String(value??'');}catch{return '';}}
|
|
29
|
+
|
|
30
|
+
function disposedError(){
|
|
31
|
+
return attachCalculatorErrorCode(
|
|
32
|
+
new Error('The calculator engine has been disposed.'),
|
|
33
|
+
CALCULATOR_ENGINE_ERROR_CODES.disposed
|
|
34
|
+
);
|
|
35
|
+
}
|
|
5
36
|
|
|
6
37
|
function tokenize(input){const source=String(input??'').trim();if(!source||source.length>512)throw new TypeError('Expression must contain 1-512 characters.');const tokens=[];let index=0;while(index<source.length){const rest=source.slice(index);const whitespace=rest.match(/^\s+/);if(whitespace){index+=whitespace[0].length;continue}const numeric=rest.match(/^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?/i);if(numeric){tokens.push({type:'number',value:Number(numeric[0])});index+=numeric[0].length;continue}const identifier=rest.match(/^[a-z]+/i);if(identifier){tokens.push({type:'name',value:identifier[0].toLowerCase()});index+=identifier[0].length;continue}const symbol=source[index];if('+-*/%^()'.includes(symbol)){tokens.push({type:symbol,value:symbol});index++;continue}throw new SyntaxError(`Unexpected character at position ${index+1}.`)}tokens.push({type:'end'});return tokens;}
|
|
7
38
|
|
|
@@ -14,7 +45,36 @@ export function evaluateExpression(input){const tokens=tokenize(input);let curso
|
|
|
14
45
|
const result=expression();if(peek().type!=='end')throw new SyntaxError('Unexpected content after the expression.');if(!Number.isFinite(result))throw new RangeError('The calculation did not produce a finite result.');return result;
|
|
15
46
|
}
|
|
16
47
|
|
|
17
|
-
export default class CalculatorEngine
|
|
18
|
-
|
|
48
|
+
export default class CalculatorEngine{
|
|
49
|
+
#disposed=false;
|
|
50
|
+
#events;
|
|
51
|
+
#operationSequence=0;
|
|
52
|
+
constructor(){this.#events=createArcaneEventSource(this,{source:'calculator-engine',eventTypes:Object.freeze(['calculator-result','calculator-error'])});}
|
|
53
|
+
addEventListener(type,listener,options){return this.#events.addEventListener(type,listener,options);}
|
|
54
|
+
removeEventListener(type,listener,options){return this.#events.removeEventListener(type,listener,options);}
|
|
55
|
+
on(type,listener,options){return this.#events.on(type,listener,options);}
|
|
56
|
+
dispatchEvent(value){return this.#events.dispatchEvent(value);}
|
|
57
|
+
calculate(expression){
|
|
58
|
+
if(this.#disposed)throw disposedError();
|
|
59
|
+
const operationId=`${this.#events.instanceId}:calculate:${(++this.#operationSequence).toString(36)}`;
|
|
60
|
+
try{
|
|
61
|
+
const calculation=new Calculation({expression,result:evaluateExpression(expression)});
|
|
62
|
+
this.#events.dispatch('calculator-result',calculation,{
|
|
63
|
+
operationId,
|
|
64
|
+
publicDetail:Object.freeze({result:calculation.result})
|
|
65
|
+
});
|
|
66
|
+
return calculation;
|
|
67
|
+
}catch(error){
|
|
68
|
+
const code=calculatorErrorCode(error);
|
|
69
|
+
attachCalculatorErrorCode(error,code);
|
|
70
|
+
this.#events.dispatch(
|
|
71
|
+
'calculator-error',
|
|
72
|
+
Object.freeze({expression:compatibilityExpression(expression),error}),
|
|
73
|
+
{operationId,publicDetail:Object.freeze({code})}
|
|
74
|
+
);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
dispose(){if(this.#disposed)return false;this.#disposed=true;return this.#events.dispose();}
|
|
79
|
+
destroy(){return this.dispose();}
|
|
19
80
|
}
|
|
20
|
-
function event(type,detail){return new CustomEvent(type,{detail});}
|