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.
Files changed (117) hide show
  1. package/CHANGELOG.md +29 -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 +3230 -397
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1141 -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 +247 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -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 +27 -27
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1162 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +6 -3
  21. package/docs/reference/behavioral-testing.md +29 -6
  22. package/docs/reference/cli.md +117 -9
  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 +108 -44
  27. package/docs/reference/inventory/runtime-modules.json +131 -53
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +157 -43
  30. package/docs/reference/runtime-components.md +258 -83
  31. package/docs/reference/runtime-modules.md +613 -77
  32. package/docs/reference/sdk-api.md +1014 -25
  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 +462 -384
  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 +639 -98
  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 +2109 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +720 -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 +470 -0
  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 +112 -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.mjs +1 -1
@@ -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
 
@@ -2,8 +2,22 @@ import {
2
2
  APP_LOCAL_STORAGE_PREFIX,
3
3
  resolveBrowserApplicationId
4
4
  } from './AppDataScope.js';
5
+ import {
6
+ createArcaneEventSource,
7
+ projectArcaneDOMEvent
8
+ } from 'arcane-os/event-manager';
9
+
10
+ const PUBLISH_DBLS_READY=Symbol('publish-dbls-ready');
11
+ export const DBLS_EVENT_TYPES=Object.freeze({
12
+ ready:'dbls-ready'
13
+ });
14
+ export const DBLS_REASONS=Object.freeze({
15
+ ready:'local-storage-adapter-ready'
16
+ });
5
17
 
6
18
  class DBLS {
19
+ #events;
20
+
7
21
  constructor({
8
22
  applicationId=null,
9
23
  documentObject=globalThis.document,
@@ -27,6 +41,10 @@ class DBLS {
27
41
  });
28
42
  this.storage=storage;
29
43
  this.storagePrefix=`${APP_LOCAL_STORAGE_PREFIX}${this.applicationId}:`;
44
+ this.#events=createArcaneEventSource(this,{
45
+ source:'dbls',
46
+ eventTypes:Object.freeze(Object.values(DBLS_EVENT_TYPES))
47
+ });
30
48
  }
31
49
 
32
50
  ready=false;
@@ -153,19 +171,34 @@ class DBLS {
153
171
  count() {
154
172
  return this.logicalKeys().length;
155
173
  }
174
+
175
+ [PUBLISH_DBLS_READY](){
176
+ const detail=Object.freeze({
177
+ dbls:this,
178
+ applicationId:this.applicationId,
179
+ reason:DBLS_REASONS.ready
180
+ });
181
+ const {occurrence}=this.#events.dispatch(
182
+ DBLS_EVENT_TYPES.ready,
183
+ detail,
184
+ {
185
+ operationId:`dbls-ready-${this.#events.instanceId}`,
186
+ publicDetail:Object.freeze({
187
+ applicationId:this.applicationId,
188
+ ready:true,
189
+ reason:DBLS_REASONS.ready
190
+ })
191
+ }
192
+ );
193
+ projectArcaneDOMEvent(window,occurrence);
194
+ }
156
195
  }
157
196
 
158
197
  if(typeof window.dbls?.get !== "function"){
159
198
  window.dbls=new DBLS();
160
199
  window.dbls.ready=true;
161
200
 
162
- const dblsReady=new CustomEvent(
163
- 'dbls-ready', {
164
- detail: { dbls: window.dbls }
165
- }
166
- );
167
-
168
- window.dispatchEvent(dblsReady);
201
+ window.dbls[PUBLISH_DBLS_READY]();
169
202
  }
170
203
 
171
204
  export default DBLS;
@@ -1,7 +1,18 @@
1
1
  import Is from "../../node_modules/strong-type/index.js";
2
2
  import {openApplicationDataDirectory} from './AppDataScope.js';
3
+ import {
4
+ createArcaneEventSource,
5
+ projectArcaneDOMEvent
6
+ } from 'arcane-os/event-manager';
3
7
  const is=new Is(false);
4
8
 
9
+ export const DBOPFS_EVENT_TYPES=Object.freeze({
10
+ ready:'dbopfs-ready'
11
+ });
12
+ export const DBOPFS_REASONS=Object.freeze({
13
+ ready:'opfs-database-ready'
14
+ });
15
+
5
16
  const DEFAULT_TABLE_DIRECTORIES=Object.freeze({
6
17
  users:'users',
7
18
  scores:'scores',
@@ -94,6 +105,8 @@ if(navigator.storage?.persist){
94
105
  */
95
106
  class DBOPFS {
96
107
 
108
+ #events;
109
+
97
110
  /** @type {FileSystemDirectoryHandle|Object} */
98
111
  #db={}
99
112
 
@@ -270,6 +283,10 @@ class DBOPFS {
270
283
  return window.dbopfs;
271
284
  }
272
285
 
286
+ this.#events=createArcaneEventSource(this,{
287
+ source:'dbopfs',
288
+ eventTypes:Object.freeze(Object.values(DBOPFS_EVENT_TYPES))
289
+ });
273
290
  this.readyPromise=this.#init(options);
274
291
  }
275
292
 
@@ -294,18 +311,25 @@ class DBOPFS {
294
311
 
295
312
  this.ready=true;
296
313
 
297
- window.dispatchEvent(
298
- new CustomEvent(
299
- 'dbopfs-ready',
300
- {
301
- detail:{
302
- dbopfs:this,
303
- applicationId:this.#applicationId,
304
- storagePath:this.#storagePath
305
- }
306
- }
307
- )
314
+ const {occurrence}=this.#events.dispatch(
315
+ DBOPFS_EVENT_TYPES.ready,
316
+ Object.freeze({
317
+ dbopfs:this,
318
+ applicationId:this.#applicationId,
319
+ storagePath:this.#storagePath,
320
+ reason:DBOPFS_REASONS.ready
321
+ }),
322
+ {
323
+ operationId:`dbopfs-ready-${this.#events.instanceId}`,
324
+ publicDetail:Object.freeze({
325
+ applicationId:this.#applicationId,
326
+ ready:true,
327
+ reason:DBOPFS_REASONS.ready,
328
+ storagePath:this.#storagePath
329
+ })
330
+ }
308
331
  );
332
+ projectArcaneDOMEvent(window,occurrence);
309
333
  }
310
334
 
311
335
  async #createDefaultTables(){
@@ -1,6 +1,7 @@
1
1
  import './DBOPFS.js';
2
2
  import {hasUserEntry} from './ChatRecords.js';
3
3
  import {hasMemoryContent} from './MemoryRecords.js';
4
+ import {arcaneEvents} from 'arcane-os/event-manager';
4
5
 
5
6
  async function clearEmptyChatsAndMemories(){
6
7
  await waitForDBOPFS();
@@ -70,12 +71,21 @@ function waitForDBOPFS(){
70
71
 
71
72
  return new Promise(
72
73
  function waitForDBOPFSPromise(resolve){
74
+ let settled=false;
75
+ let unsubscribe;
73
76
  function ready(){
74
- window.removeEventListener('dbopfs-ready',ready);
77
+ if(settled||window.dbopfs?.ready!==true){
78
+ return;
79
+ }
80
+ settled=true;
81
+ unsubscribe?.();
75
82
  resolve(window.dbopfs);
76
83
  }
77
84
 
78
- window.addEventListener('dbopfs-ready',ready);
85
+ unsubscribe=arcaneEvents.subscribe(
86
+ 'dbopfs-ready',
87
+ ready
88
+ );
79
89
 
80
90
  if(window.dbopfs?.ready){
81
91
  ready();
@@ -1,4 +1,8 @@
1
1
  import waitForComponent from './WaitForComponent.js';
2
+ import {
3
+ arcaneEvents,
4
+ createArcaneEventSource
5
+ } from 'arcane-os/event-manager';
2
6
 
3
7
  const DEFAULT_DELAY_MS=2_000;
4
8
  const DEFAULT_MAX_REPORTS_PER_SESSION=10;
@@ -10,6 +14,19 @@ const MAX_DETAIL_LENGTH=8_000;
10
14
  const HANDLER_MARKER=Symbol.for('arcane.global-errors.handler');
11
15
  const DEVELOPER_MODAL_HREF=new URL('../components/modal.html?v=13',import.meta.url).href;
12
16
 
17
+ export const GLOBAL_ERROR_EVENT_TYPES=Object.freeze({
18
+ browserErrorCaptured:'arcane-error-captured',
19
+ unhandledRejectionCaptured:'arcane-unhandled-rejection-captured'
20
+ });
21
+ export const GLOBAL_ERROR_EVENT_CODES=Object.freeze({
22
+ browserErrorCaptured:'ARCANE_BROWSER_ERROR_CAPTURED',
23
+ unhandledRejectionCaptured:'ARCANE_UNHANDLED_REJECTION_CAPTURED'
24
+ });
25
+ export const GLOBAL_ERROR_REASONS=Object.freeze({
26
+ browserErrorCaptured:'browser-error-captured',
27
+ unhandledRejectionCaptured:'unhandled-promise-rejection-captured'
28
+ });
29
+
13
30
  const MESSAGE_STYLE=[
14
31
  'Write a simple plain-text email showing the error and, when the available details support it, a possible solution.',
15
32
  'If loop_detected or error_storm_detected is true, put that warning first and clearly state that further notifications have been suppressed.',
@@ -374,6 +391,12 @@ async function presentDeveloperIncidentModal(target,incident,fingerprint){
374
391
  }
375
392
 
376
393
  class Errors {
394
+ #events;
395
+
396
+ #operationSequence=0;
397
+
398
+ #stopUserLoaded=null;
399
+
377
400
  constructor(options={}) {
378
401
  const target=options.target||globalThis.window;
379
402
  if(!target||typeof target.addEventListener!=='function'){
@@ -384,6 +407,13 @@ class Errors {
384
407
  return target.errors;
385
408
  }
386
409
 
410
+ this.#events=createArcaneEventSource(this,{
411
+ source:'global-error-handler',
412
+ eventTypes:Object.freeze([
413
+ GLOBAL_ERROR_EVENT_TYPES.browserErrorCaptured,
414
+ GLOBAL_ERROR_EVENT_TYPES.unhandledRejectionCaptured
415
+ ])
416
+ });
387
417
  this[HANDLER_MARKER]=true;
388
418
  this.target=target;
389
419
  this.delayMs=options.delayMs??DEFAULT_DELAY_MS;
@@ -674,17 +704,15 @@ class Errors {
674
704
  }
675
705
 
676
706
  this.waitingForUser=true;
677
- this.target.addEventListener(
707
+ this.#stopUserLoaded=arcaneEvents.subscribe(
678
708
  'user-entity-loaded',
679
- this.onUserLoaded
709
+ this.onUserLoaded,
710
+ {once:true}
680
711
  );
681
712
  }
682
713
 
683
714
  onUserLoaded(){
684
- this.target.removeEventListener(
685
- 'user-entity-loaded',
686
- this.onUserLoaded
687
- );
715
+ this.#stopUserLoaded=null;
688
716
  this.waitingForUser=false;
689
717
 
690
718
  const deferred=this.deferredDeveloperIncident;
@@ -754,6 +782,33 @@ class Errors {
754
782
  return false;
755
783
  }
756
784
 
785
+ const incidentKind=incident?.type==='unhandledrejection'
786
+ ?'unhandled-promise-rejection'
787
+ :'browser-error';
788
+ const eventType=incidentKind==='unhandled-promise-rejection'
789
+ ?GLOBAL_ERROR_EVENT_TYPES.unhandledRejectionCaptured
790
+ :GLOBAL_ERROR_EVENT_TYPES.browserErrorCaptured;
791
+ const code=incidentKind==='unhandled-promise-rejection'
792
+ ?GLOBAL_ERROR_EVENT_CODES.unhandledRejectionCaptured
793
+ :GLOBAL_ERROR_EVENT_CODES.browserErrorCaptured;
794
+ const reason=incidentKind==='unhandled-promise-rejection'
795
+ ?GLOBAL_ERROR_REASONS.unhandledRejectionCaptured
796
+ :GLOBAL_ERROR_REASONS.browserErrorCaptured;
797
+ this.#operationSequence+=1;
798
+ this.#events.dispatch(
799
+ eventType,
800
+ Object.freeze({...incident,code,fingerprint,reason}),
801
+ {
802
+ operationId:`global-error-handler-${this.#events.instanceId}-${this.#operationSequence.toString(36)}`,
803
+ publicDetail:Object.freeze({
804
+ id:fingerprint,
805
+ code,
806
+ kind:incidentKind,
807
+ reason
808
+ })
809
+ }
810
+ );
811
+
757
812
  this.offerDeveloperIncident(incident,fingerprint);
758
813
 
759
814
  if(this.circuitOpen){
@@ -976,7 +1031,8 @@ class Errors {
976
1031
  this.destroyed=true;
977
1032
  this.target.removeEventListener('error',this.onError,true);
978
1033
  this.target.removeEventListener('unhandledrejection',this.onRejection,true);
979
- this.target.removeEventListener('user-entity-loaded',this.onUserLoaded);
1034
+ this.#stopUserLoaded?.();
1035
+ this.#stopUserLoaded=null;
980
1036
  this.waitingForUser=false;
981
1037
  this.deferredDeveloperIncident=null;
982
1038
 
@@ -1015,6 +1071,8 @@ class Errors {
1015
1071
  this.target.errors=undefined;
1016
1072
  }
1017
1073
  }
1074
+
1075
+ this.#events.dispose();
1018
1076
  }
1019
1077
  }
1020
1078