arcane-os 0.2.3 → 0.3.1

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 (118) hide show
  1. package/CHANGELOG.md +30 -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 +3242 -391
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1209 -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 +249 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2322 -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 +18 -20
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1205 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +2 -2
  21. package/docs/reference/behavioral-testing.md +28 -5
  22. package/docs/reference/cli.md +115 -7
  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 +86 -37
  27. package/docs/reference/inventory/runtime-modules.json +126 -49
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +185 -33
  30. package/docs/reference/runtime-components.md +192 -80
  31. package/docs/reference/runtime-modules.md +589 -70
  32. package/docs/reference/sdk-api.md +1017 -24
  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 +453 -131
  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 +244 -72
  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 +2117 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +756 -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 +235 -37
  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 +130 -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-runtime.mjs +804 -22
  118. 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(new Error('The browser test run was aborted.'),'BROWSER_TEST_ABORTED');
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==='BROWSER_TEST_ABORTED'){
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.dispatchEvent(detailEvent(type,Object.freeze(detail)));
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=Boolean(settings.signal?.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
- if(aborted||settings.signal?.aborted){
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('browser-test-result',{index,result,total:this.#tests.length});
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('browser-test-start',{
283
- index,
284
- test:Object.freeze({id:descriptor.id,name:descriptor.name,timeoutMs:descriptor.timeoutMs}),
285
- total:this.#tests.length,
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:settings.signal,timeoutMs:descriptor.timeoutMs});
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==='BROWSER_TEST_ABORTED') aborted=true;
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('browser-test-result',{index,result,total:this.#tests.length});
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('browser-test-suite-complete',summary);
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 extends EventTarget{
18
- calculate(expression){try{const calculation=new Calculation({expression,result:evaluateExpression(expression)});this.dispatchEvent(event('calculator-result',calculation));return calculation}catch(error){this.dispatchEvent(event('calculator-error',{expression:String(expression??''),error}));throw error}}
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});}