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,5 +1,79 @@
1
+ import {
2
+ createArcaneEventSource,
3
+ projectArcaneDOMEvent
4
+ } from 'arcane-os/event-manager';
5
+
1
6
  const own=(value,key)=>Object.prototype.hasOwnProperty.call(value,key);
2
7
 
8
+ const STT_ACTIVATION_EVENT_TYPES=Object.freeze({
9
+ error:'speech-stt-activation-error',
10
+ request:'speech-stt-activation-request'
11
+ });
12
+
13
+ const STT_ACTIVATION_ERROR_CODES=Object.freeze({
14
+ buttonInvalid:'ARCANE_STT_ACTIVATION_BUTTON_INVALID',
15
+ buttonListenerFailed:'ARCANE_STT_ACTIVATION_BUTTON_LISTENER_FAILED',
16
+ domProjectionUnavailable:'ARCANE_STT_ACTIVATION_DOM_PROJECTION_UNAVAILABLE',
17
+ eventClassInvalid:'ARCANE_STT_ACTIVATION_EVENT_CLASS_INVALID',
18
+ eventSourceInvalid:'ARCANE_STT_ACTIVATION_EVENT_SOURCE_INVALID',
19
+ hostInvalid:'ARCANE_STT_ACTIVATION_HOST_INVALID',
20
+ onChangeInvalid:'ARCANE_STT_ACTIVATION_ON_CHANGE_INVALID',
21
+ presentationCallbackFailed:'ARCANE_STT_ACTIVATION_PRESENTATION_CALLBACK_FAILED',
22
+ requestRejected:'ARCANE_STT_ACTIVATION_REQUEST_REJECTED'
23
+ });
24
+
25
+ const STT_ACTIVATION_REASONS=Object.freeze({
26
+ explicitRequest:'explicit-request',
27
+ presentationCallbackThrew:'activation-presentation-callback-threw',
28
+ requestRejected:'activation-request-rejected'
29
+ });
30
+
31
+ function codedError(message,code,ErrorClass=Error,cause){
32
+ const error=new ErrorClass(message);
33
+ error.code=code;
34
+ if(cause!==undefined){
35
+ error.cause=cause;
36
+ }
37
+ return error;
38
+ }
39
+
40
+ function projectSTTActivationEvent(
41
+ host,
42
+ occurrence,
43
+ EventClass,
44
+ options={}
45
+ ){
46
+ if(typeof globalThis.CustomEvent!=='function'){
47
+ throw codedError(
48
+ 'The STT activation DOM compatibility projection requires CustomEvent.',
49
+ STT_ACTIVATION_ERROR_CODES.domProjectionUnavailable
50
+ );
51
+ }
52
+ if(EventClass===globalThis.CustomEvent){
53
+ return projectArcaneDOMEvent(host,occurrence,options);
54
+ }
55
+ const compatibilityTarget={
56
+ dispatchEvent(projectedEvent){
57
+ const event=new EventClass(
58
+ projectedEvent.type,
59
+ {
60
+ detail:projectedEvent.detail,
61
+ bubbles:projectedEvent.bubbles,
62
+ composed:projectedEvent.composed,
63
+ cancelable:projectedEvent.cancelable
64
+ }
65
+ );
66
+ const accepted=host.dispatchEvent(event)!==false
67
+ &&!event.defaultPrevented;
68
+ if(!accepted&&projectedEvent.cancelable){
69
+ projectedEvent.preventDefault();
70
+ }
71
+ return accepted;
72
+ }
73
+ };
74
+ return projectArcaneDOMEvent(compatibilityTarget,occurrence,options);
75
+ }
76
+
3
77
  function record(value,label){
4
78
  if(value===undefined){
5
79
  return {};
@@ -331,12 +405,66 @@ function createSTTActivationController({
331
405
  host,
332
406
  button,
333
407
  onChange,
334
- EventClass=CustomEvent
408
+ EventClass=globalThis.CustomEvent,
409
+ eventSource=null
335
410
  }){
411
+ if(!host||typeof host.dispatchEvent!=='function'
412
+ ||typeof host.requestSTTActivation!=='function'){
413
+ throw codedError(
414
+ 'The STT activation host must provide dispatchEvent() and requestSTTActivation().',
415
+ STT_ACTIVATION_ERROR_CODES.hostInvalid,
416
+ TypeError
417
+ );
418
+ }
419
+ if(typeof EventClass!=='function'){
420
+ throw codedError(
421
+ 'The STT activation event class must be a constructor.',
422
+ STT_ACTIVATION_ERROR_CODES.eventClassInvalid,
423
+ TypeError
424
+ );
425
+ }
426
+ if(!button
427
+ ||typeof button.addEventListener!=='function'
428
+ ||typeof button.removeEventListener!=='function'){
429
+ throw codedError(
430
+ 'The STT activation button must provide addEventListener() and removeEventListener().',
431
+ STT_ACTIVATION_ERROR_CODES.buttonInvalid,
432
+ TypeError
433
+ );
434
+ }
435
+ if(typeof onChange!=='function'){
436
+ throw codedError(
437
+ 'The STT activation onChange callback must be a function.',
438
+ STT_ACTIVATION_ERROR_CODES.onChangeInvalid,
439
+ TypeError
440
+ );
441
+ }
442
+ if(eventSource!==null
443
+ &&(
444
+ typeof eventSource!=='object'
445
+ ||typeof eventSource.dispatch!=='function'
446
+ ||typeof eventSource.dispose!=='function'
447
+ ||typeof eventSource.instanceId!=='string'
448
+ )){
449
+ throw codedError(
450
+ 'The STT activation event source must be a compatible Arcane event source.',
451
+ STT_ACTIVATION_ERROR_CODES.eventSourceInvalid,
452
+ TypeError
453
+ );
454
+ }
455
+ const ownsEventSource=eventSource===null;
456
+ const events=eventSource||createArcaneEventSource(
457
+ host,
458
+ {
459
+ source:'arcane.module.component-contracts.stt-activation',
460
+ eventTypes:Object.values(STT_ACTIVATION_EVENT_TYPES)
461
+ }
462
+ );
336
463
  let role=null;
337
464
  let requestPending=false;
338
465
  let requestError='';
339
466
  let requestGeneration=0;
467
+ let operationSequence=0;
340
468
  let destroyed=false;
341
469
 
342
470
  function visibleError(error,fallback){
@@ -468,34 +596,51 @@ function createSTTActivationController({
468
596
  }
469
597
  );
470
598
  const activationRequest=Object.freeze({intent,state:role});
471
- const activationEvent=new EventClass(
472
- 'speech-stt-activation-request',
473
- {
474
- bubbles:true,
475
- composed:true,
476
- cancelable:true,
477
- detail:activationRequest
478
- }
479
- );
599
+ const operationId=
600
+ `${events.instanceId}:stt-activation:${(++operationSequence).toString(36)}`;
601
+ let requestInvoked=false;
480
602
  requestPending=true;
481
- const accepted=host.dispatchEvent(activationEvent);
482
- if(!accepted
483
- ||destroyed
484
- ||generation!==requestGeneration
485
- ||action()!==nextAction){
486
- if(!destroyed&&generation===requestGeneration){
487
- requestPending=false;
488
- onChange();
489
- }
490
- return false;
491
- }
492
603
  try{
604
+ const publication=events.dispatch(
605
+ STT_ACTIVATION_EVENT_TYPES.request,
606
+ activationRequest,
607
+ {
608
+ cancelable:true,
609
+ operationId,
610
+ publicDetail:{
611
+ role:'stt',
612
+ action:nextAction,
613
+ reason:STT_ACTIVATION_REASONS.explicitRequest,
614
+ state:role.state,
615
+ roleOperationId:typeof role?.operationId==='string'
616
+ ?role.operationId
617
+ :null
618
+ }
619
+ }
620
+ );
621
+ if(!publication.accepted
622
+ ||!projectSTTActivationEvent(
623
+ host,
624
+ publication.occurrence,
625
+ EventClass,
626
+ {
627
+ bubbles:true,
628
+ composed:true,
629
+ cancelable:true
630
+ }
631
+ )
632
+ ||destroyed
633
+ ||generation!==requestGeneration
634
+ ||action()!==nextAction){
635
+ return false;
636
+ }
493
637
  onChange();
494
638
  if(destroyed
495
639
  ||generation!==requestGeneration
496
640
  ||action()!==nextAction){
497
641
  return false;
498
642
  }
643
+ requestInvoked=true;
499
644
  await host.requestSTTActivation(intent);
500
645
  if(destroyed||generation!==requestGeneration){
501
646
  return false;
@@ -515,21 +660,42 @@ function createSTTActivationController({
515
660
  `The transcription ${nextAction} request failed.`
516
661
  );
517
662
  requestError=message;
518
- host.dispatchEvent(
519
- new EventClass(
520
- 'speech-stt-activation-error',
663
+ const code=requestInvoked
664
+ ?STT_ACTIVATION_ERROR_CODES.requestRejected
665
+ :STT_ACTIVATION_ERROR_CODES.presentationCallbackFailed;
666
+ const reason=requestInvoked
667
+ ?STT_ACTIVATION_REASONS.requestRejected
668
+ :STT_ACTIVATION_REASONS.presentationCallbackThrew;
669
+ const errorPublication=events.dispatch(
670
+ STT_ACTIVATION_EVENT_TYPES.error,
671
+ Object.freeze(
521
672
  {
522
- bubbles:true,
523
- composed:true,
524
- detail:Object.freeze(
525
- {
526
- request:activationRequest,
527
- error,
528
- message
529
- }
530
- )
673
+ request:activationRequest,
674
+ error,
675
+ message
531
676
  }
532
- )
677
+ ),
678
+ {
679
+ operationId,
680
+ publicDetail:{
681
+ role:'stt',
682
+ action:nextAction,
683
+ code,
684
+ reason,
685
+ causeCode:typeof error?.code==='string'
686
+ ?error.code
687
+ :null
688
+ }
689
+ }
690
+ );
691
+ projectSTTActivationEvent(
692
+ host,
693
+ errorPublication.occurrence,
694
+ EventClass,
695
+ {
696
+ bubbles:true,
697
+ composed:true
698
+ }
533
699
  );
534
700
  return false;
535
701
  }finally{
@@ -547,7 +713,11 @@ function createSTTActivationController({
547
713
  function activateSelectedSTT(){
548
714
  const nextAction=action();
549
715
  if(nextAction){
550
- void request(nextAction);
716
+ void request(nextAction).catch(
717
+ function reportSTTActivationRequestFailure(error){
718
+ console.error('The STT activation request could not settle.',error);
719
+ }
720
+ );
551
721
  }
552
722
  }
553
723
 
@@ -575,10 +745,35 @@ function createSTTActivationController({
575
745
  destroyed=true;
576
746
  requestGeneration+=1;
577
747
  requestPending=false;
578
- button.removeEventListener('click',activateSelectedSTT);
748
+ try{
749
+ button.removeEventListener('click',activateSelectedSTT);
750
+ }catch(error){
751
+ throw codedError(
752
+ 'The STT activation button listener could not be removed.',
753
+ STT_ACTIVATION_ERROR_CODES.buttonListenerFailed,
754
+ Error,
755
+ error
756
+ );
757
+ }finally{
758
+ if(ownsEventSource){
759
+ events.dispose();
760
+ }
761
+ }
579
762
  }
580
763
 
581
- button.addEventListener('click',activateSelectedSTT);
764
+ try{
765
+ button.addEventListener('click',activateSelectedSTT);
766
+ }catch(error){
767
+ if(ownsEventSource){
768
+ events.dispose();
769
+ }
770
+ throw codedError(
771
+ 'The STT activation button listener could not be installed.',
772
+ STT_ACTIVATION_ERROR_CODES.buttonListenerFailed,
773
+ Error,
774
+ error
775
+ );
776
+ }
582
777
  return Object.freeze(
583
778
  {
584
779
  get action(){return action();},
@@ -841,6 +1036,9 @@ export {
841
1036
  DASHBOARD_LABELS,
842
1037
  MARKDOWN_FORMATS,
843
1038
  MARKDOWN_LABELS,
1039
+ STT_ACTIVATION_ERROR_CODES,
1040
+ STT_ACTIVATION_EVENT_TYPES,
1041
+ STT_ACTIVATION_REASONS,
844
1042
  VOICE_LABELS,
845
1043
  VOICE_MESSAGES,
846
1044
  appendTranscription,
@@ -1,7 +1,30 @@
1
+ import {createArcaneEventSource} from 'arcane-os/event-manager';
2
+
1
3
  const CONVERSATION_TIMEBOX_TOOL_NAME='conversation_timebox';
2
4
  const CONVERSATION_TIMEBOX_TICK_MS=1000;
3
5
  const MAX_DATE_MILLISECONDS=8_640_000_000_000_000;
4
6
 
7
+ export const CONVERSATION_TIMEBOX_EVENT_TYPES=Object.freeze({
8
+ change:'conversation-timebox-change'
9
+ });
10
+
11
+ export const CONVERSATION_TIMEBOX_ERROR_CODES=Object.freeze({
12
+ disposed:'ARCANE_CONVERSATION_TIMEBOX_DISPOSED',
13
+ subscriptionHandlerInvalid:'ARCANE_CONVERSATION_TIMEBOX_SUBSCRIPTION_HANDLER_INVALID',
14
+ subscriptionOptionsInvalid:'ARCANE_CONVERSATION_TIMEBOX_SUBSCRIPTION_OPTIONS_INVALID'
15
+ });
16
+
17
+ export const CONVERSATION_TIMEBOX_REASONS=Object.freeze({
18
+ deadlineReached:'conversation-timebox-deadline-reached',
19
+ disposed:'conversation-timebox-disposed',
20
+ elapsedTimeAdvanced:'conversation-timebox-elapsed-time-advanced',
21
+ limitAdjusted:'conversation-timebox-limit-adjusted',
22
+ limitCleared:'conversation-timebox-limit-cleared',
23
+ limitSet:'conversation-timebox-limit-set',
24
+ snapshotReplayed:'conversation-timebox-snapshot-replayed',
25
+ started:'conversation-timebox-started'
26
+ });
27
+
5
28
  const CONVERSATION_TIMEBOX_OPENING_INSTRUCTION=`Before any other intake question, ask exactly one timing question: "Do you have a limited amount of time to talk right now?" Ask no other question in that opening reply, and wait for the answer before continuing the app's normal intake. The elapsed conversation timer is already visible. A time limit applies only to this conversation. Never infer, round, or invent a duration. The conversation_timebox tool is available on every turn. When the user explicitly sets a duration, call it with action "set" and convert the exact stated duration to whole milliseconds. When the user explicitly adds time, call it with action "adjust" and convert the exact added duration to whole milliseconds. When the user explicitly says there is no limit, asks to remove it, or chooses to continue after a due check without setting another duration, call it with action "clear". A duration is always sent as duration_milliseconds; for example, 75 seconds is 75000 and 10 minutes is 600000. If the duration is vague or ranged, ask one focused question and do not call the tool yet. The timebox tool must be the sole tool call for its response. Adapt the pace and prioritize the most important next action within an active limit without skipping safety. A message that the agreed time check is due is a check-in, not a request to end: ask whether the user wants to end now or continue, and do not assume their choice.`;
6
29
 
7
30
  const CONVERSATION_TIMEBOX_LIMIT_MESSAGE='The agreed conversation time check is due. Please ask whether I want to end now or continue, and do not assume my choice or set another limit unless I explicitly state one.';
@@ -144,6 +167,51 @@ function reportConversationTimeboxListenerError(error){
144
167
  console.error('A conversation timebox listener failed.',error);
145
168
  }
146
169
 
170
+ function conversationTimeboxError(message,code,reason,ErrorType=Error){
171
+ const error=new ErrorType(message);
172
+ error.code=code;
173
+ error.reason=reason;
174
+ return error;
175
+ }
176
+
177
+ function isAbortSignal(value){
178
+ return Boolean(value)
179
+ &&typeof value==='object'
180
+ &&typeof value.aborted==='boolean'
181
+ &&typeof value.addEventListener==='function'
182
+ &&typeof value.removeEventListener==='function';
183
+ }
184
+
185
+ function normalizeSubscriptionOptions(value={}){
186
+ if(!isPlainRecord(value)){
187
+ throw conversationTimeboxError(
188
+ 'Conversation timebox subscription options must be a plain object.',
189
+ CONVERSATION_TIMEBOX_ERROR_CODES.subscriptionOptionsInvalid,
190
+ 'conversation-timebox-subscription-options-invalid',
191
+ TypeError
192
+ );
193
+ }
194
+ const unsupported=Reflect.ownKeys(value).find(function findUnsupportedTimeboxSubscriptionOption(key){
195
+ return key!=='once'&&key!=='signal';
196
+ });
197
+ if(unsupported!==undefined||(
198
+ value.once!==undefined
199
+ &&typeof value.once!=='boolean'
200
+ )||(
201
+ value.signal!==undefined
202
+ &&value.signal!==null
203
+ &&!isAbortSignal(value.signal)
204
+ )){
205
+ throw conversationTimeboxError(
206
+ 'Conversation timebox subscriptions support only boolean once and an AbortSignal.',
207
+ CONVERSATION_TIMEBOX_ERROR_CODES.subscriptionOptionsInvalid,
208
+ 'conversation-timebox-subscription-options-invalid',
209
+ TypeError
210
+ );
211
+ }
212
+ return Object.freeze({once:value.once===true,signal:value.signal??null});
213
+ }
214
+
147
215
  function defaultClock(){
148
216
  return Date.now();
149
217
  }
@@ -194,10 +262,10 @@ class ConversationTimebox{
194
262
  #dueAtMs=null;
195
263
  #dueRevision=null;
196
264
  #durationMilliseconds=null;
265
+ #events;
197
266
  #intervalHandle=null;
198
- #listeners=new Set();
199
267
  #nowMs=null;
200
- #onListenerError;
268
+ #operationSequence=0;
201
269
  #revision=0;
202
270
  #schedule;
203
271
  #setAtMs=null;
@@ -219,14 +287,14 @@ class ConversationTimebox{
219
287
  this.#clock=options.clock??defaultClock;
220
288
  this.#schedule=options.schedule??defaultSchedule;
221
289
  this.#cancel=options.cancel??defaultCancel;
222
- this.#onListenerError=options.onListenerError??reportConversationTimeboxListenerError;
290
+ const onListenerError=options.onListenerError??reportConversationTimeboxListenerError;
223
291
  this.#tickMs=options.tickMs??CONVERSATION_TIMEBOX_TICK_MS;
224
292
 
225
293
  for(const [name,value] of [
226
294
  ['clock',this.#clock],
227
295
  ['schedule',this.#schedule],
228
296
  ['cancel',this.#cancel],
229
- ['onListenerError',this.#onListenerError]
297
+ ['onListenerError',onListenerError]
230
298
  ]){
231
299
  if(typeof value!=='function'){
232
300
  throw new TypeError(`${name} must be a function.`);
@@ -235,6 +303,11 @@ class ConversationTimebox{
235
303
  if(!Number.isSafeInteger(this.#tickMs)||this.#tickMs<100||this.#tickMs>60_000){
236
304
  throw new RangeError('tickMs must be an integer between 100 and 60000.');
237
305
  }
306
+ this.#events=createArcaneEventSource(this,{
307
+ source:'conversation-timebox',
308
+ eventTypes:Object.freeze(Object.values(CONVERSATION_TIMEBOX_EVENT_TYPES)),
309
+ onListenerError
310
+ });
238
311
  }
239
312
 
240
313
  get limitReachedMessage(){
@@ -251,7 +324,7 @@ class ConversationTimebox{
251
324
  return this.snapshot();
252
325
  }
253
326
  const now=this.#readClock();
254
- this.#startAt(now,true);
327
+ this.#startAt(now,true,CONVERSATION_TIMEBOX_REASONS.started);
255
328
  return this.snapshot();
256
329
  }
257
330
 
@@ -263,13 +336,14 @@ class ConversationTimebox{
263
336
  if(typeof source!=='string'||!source.trim()){
264
337
  throw new TypeError('Conversation timebox source must contain text.');
265
338
  }
339
+ const commandSource=source.trim();
266
340
  const now=this.#readClock();
267
341
  const dueAtMs=now+milliseconds;
268
342
  if(!Number.isSafeInteger(dueAtMs)||dueAtMs>MAX_DATE_MILLISECONDS){
269
343
  throw new RangeError('The requested conversation duration exceeds the supported date range.');
270
344
  }
271
345
  if(this.#startedAtMs===null){
272
- this.#startAt(now,false);
346
+ this.#startAt(now,false,CONVERSATION_TIMEBOX_REASONS.started);
273
347
  }
274
348
 
275
349
  this.#durationMilliseconds=milliseconds;
@@ -278,7 +352,7 @@ class ConversationTimebox{
278
352
  this.#dueRevision=null;
279
353
  this.#revision++;
280
354
  this.#nowMs=now;
281
- this.#notify('change');
355
+ this.#notify('change',CONVERSATION_TIMEBOX_REASONS.limitSet,commandSource);
282
356
  return this.snapshot();
283
357
  }
284
358
 
@@ -290,6 +364,7 @@ class ConversationTimebox{
290
364
  if(typeof source!=='string'||!source.trim()){
291
365
  throw new TypeError('Conversation timebox source must contain text.');
292
366
  }
367
+ const commandSource=source.trim();
293
368
  if(this.#dueAtMs===null){
294
369
  throw new Error('An active conversation time check is required before adding time.');
295
370
  }
@@ -306,7 +381,7 @@ class ConversationTimebox{
306
381
  this.#dueRevision=null;
307
382
  this.#revision++;
308
383
  this.#nowMs=now;
309
- this.#notify('change');
384
+ this.#notify('change',CONVERSATION_TIMEBOX_REASONS.limitAdjusted,commandSource);
310
385
  return this.snapshot();
311
386
  }
312
387
 
@@ -315,6 +390,7 @@ class ConversationTimebox{
315
390
  if(typeof source!=='string'||!source.trim()){
316
391
  throw new TypeError('Conversation timebox source must contain text.');
317
392
  }
393
+ const commandSource=source.trim();
318
394
  if(this.#startedAtMs===null){
319
395
  this.start();
320
396
  }
@@ -324,7 +400,7 @@ class ConversationTimebox{
324
400
  this.#dueRevision=null;
325
401
  this.#revision++;
326
402
  this.#nowMs=this.#readClock();
327
- this.#notify('change');
403
+ this.#notify('change',CONVERSATION_TIMEBOX_REASONS.limitCleared,commandSource);
328
404
  return this.snapshot();
329
405
  }
330
406
 
@@ -344,19 +420,41 @@ class ConversationTimebox{
344
420
  );
345
421
  }
346
422
 
347
- subscribe(listener){
423
+ subscribe(listener,options={}){
348
424
  if(typeof listener!=='function'){
349
- throw new TypeError('Conversation timebox listener must be a function.');
425
+ throw conversationTimeboxError(
426
+ 'Conversation timebox listener must be a function.',
427
+ CONVERSATION_TIMEBOX_ERROR_CODES.subscriptionHandlerInvalid,
428
+ 'conversation-timebox-subscription-handler-invalid',
429
+ TypeError
430
+ );
350
431
  }
351
- this.#listeners.add(listener);
352
- listener(
353
- this.snapshot(),
354
- Object.freeze({type:'snapshot',revision:this.#revision})
432
+ this.#assertActive();
433
+ const normalized=normalizeSubscriptionOptions(options);
434
+ function forwardConversationTimeboxChange(event){
435
+ listener(event.detail.state,event.detail.event);
436
+ }
437
+ const unsubscribe=this.#events.on(
438
+ CONVERSATION_TIMEBOX_EVENT_TYPES.change,
439
+ forwardConversationTimeboxChange,
440
+ normalized
355
441
  );
356
- const listeners=this.#listeners;
357
- return function unsubscribeConversationTimeboxListener(){
358
- return listeners.delete(listener);
359
- };
442
+ if(normalized.signal?.aborted)return unsubscribe;
443
+ try{
444
+ listener(
445
+ this.snapshot(),
446
+ Object.freeze({
447
+ type:'snapshot',
448
+ reason:CONVERSATION_TIMEBOX_REASONS.snapshotReplayed,
449
+ revision:this.#revision,
450
+ commandSource:null
451
+ })
452
+ );
453
+ }catch(error){
454
+ unsubscribe();
455
+ throw error;
456
+ }
457
+ return unsubscribe;
360
458
  }
361
459
 
362
460
  snapshot(){
@@ -399,15 +497,19 @@ class ConversationTimebox{
399
497
  this.#intervalHandle=null;
400
498
  }
401
499
  this.#disposed=true;
402
- this.#notify('change');
500
+ this.#notify('change',CONVERSATION_TIMEBOX_REASONS.disposed);
403
501
  const finalSnapshot=this.snapshot();
404
- this.#listeners.clear();
502
+ this.#events.dispose();
405
503
  return finalSnapshot;
406
504
  }
407
505
 
408
506
  #assertActive(){
409
507
  if(this.#disposed){
410
- throw new Error('The conversation timebox has been disposed.');
508
+ throw conversationTimeboxError(
509
+ 'The conversation timebox has been disposed.',
510
+ CONVERSATION_TIMEBOX_ERROR_CODES.disposed,
511
+ CONVERSATION_TIMEBOX_REASONS.disposed
512
+ );
411
513
  }
412
514
  }
413
515
 
@@ -419,7 +521,7 @@ class ConversationTimebox{
419
521
  return this.#nowMs===null?now:Math.max(now,this.#nowMs);
420
522
  }
421
523
 
422
- #startAt(now,notify){
524
+ #startAt(now,notify,reason){
423
525
  const intervalHandle=this.#schedule(
424
526
  this.#tick.bind(this),
425
527
  this.#tickMs
@@ -428,7 +530,7 @@ class ConversationTimebox{
428
530
  this.#nowMs=now;
429
531
  this.#intervalHandle=intervalHandle;
430
532
  if(notify){
431
- this.#notify('change');
533
+ this.#notify('change',reason);
432
534
  }
433
535
  }
434
536
 
@@ -446,19 +548,36 @@ class ConversationTimebox{
446
548
  this.#dueRevision=this.#revision;
447
549
  becameDue=true;
448
550
  }
449
- this.#notify(becameDue?'due':'tick');
551
+ this.#notify(
552
+ becameDue?'due':'tick',
553
+ becameDue
554
+ ?CONVERSATION_TIMEBOX_REASONS.deadlineReached
555
+ :CONVERSATION_TIMEBOX_REASONS.elapsedTimeAdvanced
556
+ );
450
557
  }
451
558
 
452
- #notify(type='change'){
559
+ #notify(type,reason,commandSource=null){
453
560
  const state=this.snapshot();
454
- const event=Object.freeze({type,revision:this.#revision});
455
- for(const listener of this.#listeners){
456
- try{
457
- listener(state,event);
458
- }catch(error){
459
- this.#onListenerError(error);
561
+ const event=Object.freeze({
562
+ type,
563
+ reason,
564
+ revision:this.#revision,
565
+ commandSource
566
+ });
567
+ const operationId=`${this.#events.instanceId}:change:${(++this.#operationSequence).toString(36)}`;
568
+ this.#events.dispatch(
569
+ CONVERSATION_TIMEBOX_EVENT_TYPES.change,
570
+ Object.freeze({state,event}),
571
+ {
572
+ operationId,
573
+ publicDetail:Object.freeze({
574
+ reason,
575
+ status:state.status,
576
+ revision:state.revision,
577
+ commandSource
578
+ })
460
579
  }
461
- }
580
+ );
462
581
  }
463
582
  }
464
583