arcane-os 0.3.3 → 0.3.5
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 +22 -0
- package/README.md +7 -7
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +315 -119
- package/browser-runtime/ai/model-controller.mjs +439 -95
- package/package.json +1 -1
- package/runtime/arcane/components/assistant-panel.html +2 -1
- package/runtime/arcane/components/chat.html +466 -206
- package/runtime/arcane/components/speech.html +107 -30
- package/runtime/arcane/components/voice-transcription.html +27 -3
- package/runtime/arcane/entities/Chat.js +165 -97
- package/runtime/arcane/entities/IntentEnvelope.js +52 -118
- package/runtime/arcane/entities/TWiNPolicyDecision.js +33 -130
- package/runtime/arcane/entities/User.js +38 -44
- package/runtime/arcane/modules/AI.js +569 -302
- package/runtime/arcane/modules/AIProviderRuntime.js +765 -135
- package/runtime/arcane/modules/AIRuntimeState.js +22 -24
- package/runtime/arcane/modules/ApiModelDatabase.js +54 -48
- package/runtime/arcane/modules/CaseEvidenceIndexer.js +6 -10
- package/runtime/arcane/modules/CommunicationHub.js +90 -94
- package/runtime/arcane/modules/ComponentContracts.js +34 -9
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +77 -77
- package/runtime/arcane/modules/ConversationTimebox.js +76 -104
- package/runtime/arcane/modules/DBLS.js +14 -12
- package/runtime/arcane/modules/DBOPFS.js +20 -15
- package/runtime/arcane/modules/Errors.js +196 -436
- package/runtime/arcane/modules/HTMLImport.js +49 -32
- package/runtime/arcane/modules/Ollama.js +16 -14
- package/runtime/arcane/modules/PersistentAIChatSession.js +174 -93
- package/runtime/arcane/modules/RecordReviewStore.js +40 -35
- package/runtime/arcane/modules/TerminalClient.js +12 -14
- package/runtime/arcane/modules/ThemeBootstrap.js +7 -7
- package/runtime/arcane/modules/ThemeManager.js +5 -5
- package/runtime/arcane/modules/TimeGuard.js +5 -65
- package/runtime/arcane/modules/WaitForComponent.js +43 -40
- package/src/cli/main.mjs +12 -11
- package/src/installed-sdk-runtime.mjs +11 -1
- package/src/mail-server.mjs +12 -5
- package/src/mail.mjs +25 -19
- package/src/workspace.mjs +32 -9
|
@@ -5,31 +5,34 @@ import {
|
|
|
5
5
|
} from 'arcane-os/event-manager';
|
|
6
6
|
|
|
7
7
|
const DEFAULT_DELAY_MS=2_000;
|
|
8
|
-
const DEFAULT_MAX_REPORTS_PER_SESSION=10;
|
|
9
|
-
const DEFAULT_MAX_REPORTS_PER_WINDOW=3;
|
|
10
|
-
const DEFAULT_MAX_PENDING_INCIDENTS=25;
|
|
11
|
-
const DEFAULT_RATE_WINDOW_MS=60_000;
|
|
12
8
|
const LEDGER_STORAGE_KEY='arcane-global-errors-v1';
|
|
13
|
-
const MAX_DETAIL_LENGTH=8_000;
|
|
14
9
|
const HANDLER_MARKER=Symbol.for('arcane.global-errors.handler');
|
|
15
10
|
const DEVELOPER_MODAL_HREF=new URL('../components/modal.html?v=13',import.meta.url).href;
|
|
16
|
-
|
|
17
|
-
export const GLOBAL_ERROR_EVENT_TYPES=Object.freeze({
|
|
11
|
+
const ERROR_EVENT_TYPES={
|
|
18
12
|
browserErrorCaptured:'arcane-error-captured',
|
|
19
13
|
unhandledRejectionCaptured:'arcane-unhandled-rejection-captured'
|
|
20
|
-
}
|
|
21
|
-
|
|
14
|
+
};
|
|
15
|
+
const ERROR_EVENT_CODES={
|
|
22
16
|
browserErrorCaptured:'ARCANE_BROWSER_ERROR_CAPTURED',
|
|
23
17
|
unhandledRejectionCaptured:'ARCANE_UNHANDLED_REJECTION_CAPTURED'
|
|
24
|
-
}
|
|
25
|
-
|
|
18
|
+
};
|
|
19
|
+
const ERROR_REASONS={
|
|
26
20
|
browserErrorCaptured:'browser-error-captured',
|
|
27
21
|
unhandledRejectionCaptured:'unhandled-promise-rejection-captured'
|
|
28
|
-
}
|
|
22
|
+
};
|
|
23
|
+
const RUNTIME_OCCURRENCE_PREFIX=(
|
|
24
|
+
typeof globalThis.crypto?.randomUUID==='function'
|
|
25
|
+
? globalThis.crypto.randomUUID()
|
|
26
|
+
: `${Date.now().toString(36)}-${Math.random().toString(36).replace(/^0\./,'')}`
|
|
27
|
+
);
|
|
28
|
+
let runtimeOccurrenceSequence=0;
|
|
29
|
+
|
|
30
|
+
export const GLOBAL_ERROR_EVENT_TYPES={...ERROR_EVENT_TYPES};
|
|
31
|
+
export const GLOBAL_ERROR_EVENT_CODES={...ERROR_EVENT_CODES};
|
|
32
|
+
export const GLOBAL_ERROR_REASONS={...ERROR_REASONS};
|
|
29
33
|
|
|
30
34
|
const MESSAGE_STYLE=[
|
|
31
35
|
'Write a simple plain-text email showing the error and, when the available details support it, a possible solution.',
|
|
32
|
-
'If loop_detected or error_storm_detected is true, put that warning first and clearly state that further notifications have been suppressed.',
|
|
33
36
|
'Do not add facts that are not present in the report data.',
|
|
34
37
|
].join(' ');
|
|
35
38
|
|
|
@@ -40,7 +43,7 @@ function safeText(value,fallback=''){
|
|
|
40
43
|
|
|
41
44
|
try{
|
|
42
45
|
const text=typeof value==='string' ? value:String(value);
|
|
43
|
-
return text
|
|
46
|
+
return text||fallback;
|
|
44
47
|
}catch{
|
|
45
48
|
return fallback;
|
|
46
49
|
}
|
|
@@ -126,92 +129,17 @@ export function normalizeRejectionEvent(event={},target=globalThis.window){
|
|
|
126
129
|
};
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
function normalizeFingerprintPart(value){
|
|
130
|
-
return safeText(value)
|
|
131
|
-
.trim()
|
|
132
|
-
.replace(/\s+/g,' ')
|
|
133
|
-
.slice(0,2_000);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function normalizeVolatileFingerprintPart(value){
|
|
137
|
-
return normalizeFingerprintPart(value)
|
|
138
|
-
.replace(/[?#][^\s)]+/g,'')
|
|
139
|
-
.replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi,'<uuid>')
|
|
140
|
-
.replace(/\b[0-9a-f]{16,}\b/gi,'<hex>')
|
|
141
|
-
.replace(/\b\d+(?:\.\d+)?\b/g,'<number>');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function normalizeStackFingerprint(value,name,message){
|
|
145
|
-
const stack=safeText(value).trim();
|
|
146
|
-
if(!stack){
|
|
147
|
-
return '';
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const lines=stack.split(/\r?\n/);
|
|
151
|
-
const firstLine=lines[0].trim();
|
|
152
|
-
const errorName=safeText(name).trim();
|
|
153
|
-
const errorMessage=safeText(message).trim();
|
|
154
|
-
const hasMessageHeader=lines.length>1&&(
|
|
155
|
-
firstLine===errorMessage
|
|
156
|
-
|| firstLine===`${errorName}: ${errorMessage}`
|
|
157
|
-
|| (errorName&&firstLine.startsWith(`${errorName}:`))
|
|
158
|
-
|| /^(?:Error|[A-Za-z_$][\w.$]*(?:Error|Exception)):(?:\s|$)/.test(firstLine)
|
|
159
|
-
);
|
|
160
|
-
const frames=hasMessageHeader ? lines.slice(1):lines;
|
|
161
|
-
|
|
162
|
-
return normalizeFingerprintPart(frames.join('\n'))
|
|
163
|
-
.replace(/[?#][^\s)]+/g,'')
|
|
164
|
-
.replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi,'<uuid>')
|
|
165
|
-
.replace(/\b[0-9a-f]{16,}\b/gi,'<hex>');
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function hashText(value){
|
|
169
|
-
let first=0x811c9dc5;
|
|
170
|
-
let second=0x9e3779b9;
|
|
171
|
-
|
|
172
|
-
for(let index=0;index<value.length;index++){
|
|
173
|
-
const code=value.charCodeAt(index);
|
|
174
|
-
first=Math.imul(first^code,0x01000193);
|
|
175
|
-
second=Math.imul(second^code,0x85ebca6b);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return [first,second]
|
|
179
|
-
.map(hash => (hash>>>0).toString(16).padStart(8,'0'))
|
|
180
|
-
.join('');
|
|
181
|
-
}
|
|
182
|
-
|
|
183
132
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
133
|
+
* Return a fresh opaque occurrence identifier for legacy callers of the former
|
|
134
|
+
* fingerprint helper. The incident is intentionally not read: identifiers do
|
|
135
|
+
* not group, admit, or derive identity from error content.
|
|
187
136
|
*
|
|
188
137
|
* @param {Object} incident
|
|
189
138
|
* @returns {string}
|
|
190
139
|
*/
|
|
191
|
-
export function fingerprintIncident(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
incident?.name,
|
|
195
|
-
incident?.message
|
|
196
|
-
);
|
|
197
|
-
const hasPreciseSource=(
|
|
198
|
-
Number.isFinite(incident?.lineno)
|
|
199
|
-
|| Number.isFinite(incident?.colno)
|
|
200
|
-
|| Boolean(stackFingerprint)
|
|
201
|
-
);
|
|
202
|
-
const signature=[
|
|
203
|
-
normalizeFingerprintPart(incident?.type),
|
|
204
|
-
normalizeFingerprintPart(incident?.name),
|
|
205
|
-
hasPreciseSource
|
|
206
|
-
? ''
|
|
207
|
-
: normalizeVolatileFingerprintPart(incident?.message),
|
|
208
|
-
normalizeFingerprintPart(incident?.filename).replace(/[?#].*$/,''),
|
|
209
|
-
normalizeFingerprintPart(incident?.lineno),
|
|
210
|
-
normalizeFingerprintPart(incident?.colno),
|
|
211
|
-
stackFingerprint,
|
|
212
|
-
].join('|');
|
|
213
|
-
|
|
214
|
-
return `error-${hashText(signature)}`;
|
|
140
|
+
export function fingerprintIncident(_incident){
|
|
141
|
+
runtimeOccurrenceSequence+=1;
|
|
142
|
+
return `error-${RUNTIME_OCCURRENCE_PREFIX}-${runtimeOccurrenceSequence.toString(36)}`;
|
|
215
143
|
}
|
|
216
144
|
|
|
217
145
|
function defaultStorage(target){
|
|
@@ -262,7 +190,7 @@ function appendDeveloperDetail(document,list,label,value){
|
|
|
262
190
|
list.append(term,description);
|
|
263
191
|
}
|
|
264
192
|
|
|
265
|
-
function buildDeveloperIncidentContent(document,incident,
|
|
193
|
+
function buildDeveloperIncidentContent(document,incident,occurrenceId){
|
|
266
194
|
const content=document.createElement('section');
|
|
267
195
|
const heading=document.createElement('h2');
|
|
268
196
|
const introduction=document.createElement('p');
|
|
@@ -282,7 +210,7 @@ function buildDeveloperIncidentContent(document,incident,fingerprint){
|
|
|
282
210
|
appendDeveloperDetail(document,details,'Name',incident?.name);
|
|
283
211
|
appendDeveloperDetail(document,details,'Message',incident?.message);
|
|
284
212
|
appendDeveloperDetail(document,details,'Source',source);
|
|
285
|
-
appendDeveloperDetail(document,details,'
|
|
213
|
+
appendDeveloperDetail(document,details,'Occurrence',occurrenceId);
|
|
286
214
|
|
|
287
215
|
content.append(heading,introduction,details);
|
|
288
216
|
|
|
@@ -324,7 +252,7 @@ async function ensureHTMLImport(target){
|
|
|
324
252
|
}
|
|
325
253
|
}
|
|
326
254
|
|
|
327
|
-
async function presentDeveloperIncidentModal(target,incident,
|
|
255
|
+
async function presentDeveloperIncidentModal(target,incident,occurrenceId){
|
|
328
256
|
const document=target?.document;
|
|
329
257
|
const container=document?.body||document?.documentElement;
|
|
330
258
|
|
|
@@ -335,11 +263,11 @@ async function presentDeveloperIncidentModal(target,incident,fingerprint){
|
|
|
335
263
|
await ensureHTMLImport(target);
|
|
336
264
|
|
|
337
265
|
const modal=document.createElement('html-import');
|
|
338
|
-
const content=buildDeveloperIncidentContent(document,incident,
|
|
266
|
+
const content=buildDeveloperIncidentContent(document,incident,occurrenceId);
|
|
339
267
|
|
|
340
268
|
modal.className='modal developer-error-modal';
|
|
341
269
|
modal.setAttribute('aria-label','Application error');
|
|
342
|
-
modal.setAttribute('data-global-error-modal',
|
|
270
|
+
modal.setAttribute('data-global-error-modal',occurrenceId);
|
|
343
271
|
modal.setAttribute('data-once','');
|
|
344
272
|
modal.setAttribute('href',DEVELOPER_MODAL_HREF);
|
|
345
273
|
|
|
@@ -409,26 +337,14 @@ class Errors {
|
|
|
409
337
|
|
|
410
338
|
this.#events=createArcaneEventSource(this,{
|
|
411
339
|
source:'global-error-handler',
|
|
412
|
-
eventTypes:
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
]
|
|
340
|
+
eventTypes:[
|
|
341
|
+
ERROR_EVENT_TYPES.browserErrorCaptured,
|
|
342
|
+
ERROR_EVENT_TYPES.unhandledRejectionCaptured
|
|
343
|
+
]
|
|
416
344
|
});
|
|
417
345
|
this[HANDLER_MARKER]=true;
|
|
418
346
|
this.target=target;
|
|
419
347
|
this.delayMs=options.delayMs??DEFAULT_DELAY_MS;
|
|
420
|
-
this.deliveryTimeoutMs=options.deliveryTimeoutMs??45_000;
|
|
421
|
-
this.deliverySchedule=options.deliverySchedule
|
|
422
|
-
|| globalThis.setTimeout.bind(globalThis);
|
|
423
|
-
this.deliveryCancel=options.deliveryCancel
|
|
424
|
-
|| globalThis.clearTimeout.bind(globalThis);
|
|
425
|
-
this.maxPendingIncidents=options.maxPendingIncidents
|
|
426
|
-
?? DEFAULT_MAX_PENDING_INCIDENTS;
|
|
427
|
-
this.maxReportsPerSession=options.maxReportsPerSession
|
|
428
|
-
?? DEFAULT_MAX_REPORTS_PER_SESSION;
|
|
429
|
-
this.maxReportsPerWindow=options.maxReportsPerWindow
|
|
430
|
-
?? DEFAULT_MAX_REPORTS_PER_WINDOW;
|
|
431
|
-
this.rateWindowMs=options.rateWindowMs??DEFAULT_RATE_WINDOW_MS;
|
|
432
348
|
this.now=options.now||Date.now;
|
|
433
349
|
this.schedule=options.schedule||globalThis.setTimeout.bind(globalThis);
|
|
434
350
|
this.cancel=options.cancel||globalThis.clearTimeout.bind(globalThis);
|
|
@@ -452,21 +368,15 @@ class Errors {
|
|
|
452
368
|
this.pending=new Map();
|
|
453
369
|
this.deliveryQueue=Promise.resolve();
|
|
454
370
|
this.destroyed=false;
|
|
455
|
-
this.
|
|
456
|
-
this.
|
|
457
|
-
this.developerModalActive=false;
|
|
458
|
-
this.developerUiDisabled=false;
|
|
459
|
-
this.developerShownFingerprints=new Set();
|
|
371
|
+
this.developerIncidentQueue=[];
|
|
372
|
+
this.developerPresentationActive=false;
|
|
460
373
|
this.waitingForUser=false;
|
|
461
374
|
|
|
462
375
|
const ledger=this.loadLedger();
|
|
463
|
-
this.reported=new Set(ledger.fingerprints);
|
|
464
|
-
this.reportTimestamps=ledger.reportTimestamps;
|
|
465
|
-
this.circuitOpen=ledger.circuitOpen||!this.storageHealthy;
|
|
466
376
|
|
|
467
377
|
if(!this.storageHealthy){
|
|
468
378
|
this.warn(
|
|
469
|
-
'
|
|
379
|
+
'Pending error delivery storage is unavailable; delivery will continue in memory.'
|
|
470
380
|
);
|
|
471
381
|
}
|
|
472
382
|
|
|
@@ -498,12 +408,7 @@ class Errors {
|
|
|
498
408
|
}
|
|
499
409
|
|
|
500
410
|
loadLedger(){
|
|
501
|
-
const empty={
|
|
502
|
-
fingerprints:[],
|
|
503
|
-
pending:[],
|
|
504
|
-
reportTimestamps:[],
|
|
505
|
-
circuitOpen:false,
|
|
506
|
-
};
|
|
411
|
+
const empty={ pending:[] };
|
|
507
412
|
|
|
508
413
|
if(!this.storageHealthy){
|
|
509
414
|
return empty;
|
|
@@ -516,25 +421,12 @@ class Errors {
|
|
|
516
421
|
}
|
|
517
422
|
|
|
518
423
|
return {
|
|
519
|
-
fingerprints:Array.isArray(value.fingerprints)
|
|
520
|
-
? value.fingerprints.filter(item => typeof item==='string')
|
|
521
|
-
: [],
|
|
522
424
|
pending:Array.isArray(value.pending)
|
|
523
|
-
? value.pending
|
|
524
|
-
|
|
525
|
-
&& typeof record==='object'
|
|
526
|
-
&& typeof record.fingerprint==='string'
|
|
527
|
-
&& record.incident
|
|
528
|
-
&& typeof record.incident==='object'
|
|
529
|
-
))
|
|
530
|
-
: [],
|
|
531
|
-
reportTimestamps:Array.isArray(value.reportTimestamps)
|
|
532
|
-
? value.reportTimestamps.filter(Number.isFinite)
|
|
533
|
-
: [],
|
|
534
|
-
circuitOpen:value.circuitOpen===true,
|
|
425
|
+
? value.pending
|
|
426
|
+
: []
|
|
535
427
|
};
|
|
536
|
-
}catch{
|
|
537
|
-
this.
|
|
428
|
+
}catch(error){
|
|
429
|
+
this.warn('Unable to restore pending error deliveries.',error);
|
|
538
430
|
return empty;
|
|
539
431
|
}
|
|
540
432
|
}
|
|
@@ -545,50 +437,22 @@ class Errors {
|
|
|
545
437
|
}
|
|
546
438
|
|
|
547
439
|
try{
|
|
548
|
-
|
|
549
|
-
fingerprints:[...this.reported],
|
|
440
|
+
this.storage.setItem(LEDGER_STORAGE_KEY,JSON.stringify({
|
|
550
441
|
pending:[...this.pending.values()].map(record => ({
|
|
551
|
-
|
|
442
|
+
capturedAt:record.capturedAt,
|
|
552
443
|
dueAt:record.dueAt,
|
|
553
|
-
errorStormDetected:record.errorStormDetected,
|
|
554
|
-
fingerprint:record.fingerprint,
|
|
555
|
-
firstSeen:record.firstSeen,
|
|
556
444
|
incident:record.incident,
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}))
|
|
560
|
-
|
|
561
|
-
circuitOpen:this.circuitOpen,
|
|
562
|
-
});
|
|
563
|
-
this.storage.setItem(LEDGER_STORAGE_KEY,serialized);
|
|
564
|
-
if(this.storage.getItem(LEDGER_STORAGE_KEY)!==serialized){
|
|
565
|
-
throw new Error('Suppression ledger verification failed');
|
|
566
|
-
}
|
|
445
|
+
occurrenceId:record.occurrenceId,
|
|
446
|
+
retryRequired:record.retryRequired===true
|
|
447
|
+
}))
|
|
448
|
+
}));
|
|
567
449
|
return true;
|
|
568
450
|
}catch(error){
|
|
569
|
-
this.
|
|
451
|
+
this.warn('Unable to persist pending error deliveries.',error);
|
|
570
452
|
return false;
|
|
571
453
|
}
|
|
572
454
|
}
|
|
573
455
|
|
|
574
|
-
disableForStorageFailure(error){
|
|
575
|
-
const shouldWarn=this.storageHealthy;
|
|
576
|
-
this.storageHealthy=false;
|
|
577
|
-
this.circuitOpen=true;
|
|
578
|
-
|
|
579
|
-
for(const record of this.pending.values()){
|
|
580
|
-
this.cancelTimer(record);
|
|
581
|
-
}
|
|
582
|
-
this.pending.clear();
|
|
583
|
-
|
|
584
|
-
if(shouldWarn){
|
|
585
|
-
this.warn(
|
|
586
|
-
'Suppression storage failed; error email notifications are disabled to prevent reload loops.',
|
|
587
|
-
error
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
456
|
cancelTimer(record){
|
|
593
457
|
if(record?.timer===null||record?.timer===undefined){
|
|
594
458
|
return;
|
|
@@ -608,7 +472,7 @@ class Errors {
|
|
|
608
472
|
record.timer=this.schedule(
|
|
609
473
|
() => {
|
|
610
474
|
try{
|
|
611
|
-
this.
|
|
475
|
+
this.flushOccurrence(record.occurrenceId);
|
|
612
476
|
}catch(error){
|
|
613
477
|
this.warn('Unable to flush a scheduled error notification.',error);
|
|
614
478
|
}
|
|
@@ -617,45 +481,61 @@ class Errors {
|
|
|
617
481
|
);
|
|
618
482
|
return true;
|
|
619
483
|
}catch(error){
|
|
620
|
-
this.pending.delete(record.fingerprint);
|
|
621
484
|
this.warn('Unable to schedule an error notification.',error);
|
|
485
|
+
record.timer=null;
|
|
486
|
+
this.flushOccurrence(record.occurrenceId);
|
|
622
487
|
return false;
|
|
623
488
|
}
|
|
624
489
|
}
|
|
625
490
|
|
|
626
491
|
restorePending(records){
|
|
627
|
-
if(
|
|
492
|
+
if(!Array.isArray(records)){
|
|
628
493
|
return;
|
|
629
494
|
}
|
|
630
495
|
|
|
631
496
|
const timestamp=this.now();
|
|
632
|
-
for(const storedRecord of records
|
|
633
|
-
if(
|
|
497
|
+
for(const storedRecord of records){
|
|
498
|
+
if(
|
|
499
|
+
!storedRecord
|
|
500
|
+
|| typeof storedRecord!=='object'
|
|
501
|
+
|| !storedRecord.incident
|
|
502
|
+
|| typeof storedRecord.incident!=='object'
|
|
503
|
+
){
|
|
504
|
+
this.warn('A persisted error delivery could not be restored.');
|
|
634
505
|
continue;
|
|
635
506
|
}
|
|
636
507
|
|
|
637
|
-
const
|
|
638
|
-
? storedRecord.
|
|
508
|
+
const capturedAt=Number.isFinite(storedRecord.capturedAt)
|
|
509
|
+
? storedRecord.capturedAt
|
|
510
|
+
: Number.isFinite(storedRecord.firstSeen)
|
|
511
|
+
? storedRecord.firstSeen
|
|
512
|
+
: timestamp;
|
|
639
513
|
const dueAt=Number.isFinite(storedRecord.dueAt)
|
|
640
|
-
? storedRecord.dueAt:
|
|
514
|
+
? storedRecord.dueAt:capturedAt+this.delayMs;
|
|
515
|
+
let occurrenceId=(
|
|
516
|
+
typeof storedRecord.occurrenceId==='string'
|
|
517
|
+
&& storedRecord.occurrenceId.trim()
|
|
518
|
+
)
|
|
519
|
+
? storedRecord.occurrenceId
|
|
520
|
+
: fingerprintIncident(storedRecord.incident);
|
|
521
|
+
while(this.pending.has(occurrenceId)){
|
|
522
|
+
occurrenceId=fingerprintIncident(storedRecord.incident);
|
|
523
|
+
}
|
|
641
524
|
const record={
|
|
642
|
-
|
|
643
|
-
|
|
525
|
+
capturedAt,
|
|
526
|
+
delivering:false,
|
|
644
527
|
dueAt,
|
|
645
|
-
errorStormDetected:storedRecord.errorStormDetected===true,
|
|
646
|
-
fingerprint:storedRecord.fingerprint,
|
|
647
|
-
firstSeen,
|
|
648
528
|
incident:storedRecord.incident,
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
timer:null
|
|
652
|
-
uniqueIncidentCount:Number.isFinite(storedRecord.uniqueIncidentCount)
|
|
653
|
-
? Math.max(1,storedRecord.uniqueIncidentCount):1,
|
|
529
|
+
occurrenceId,
|
|
530
|
+
retryRequired:storedRecord.retryRequired===true,
|
|
531
|
+
timer:null
|
|
654
532
|
};
|
|
655
533
|
|
|
656
|
-
this.pending.set(record.
|
|
657
|
-
|
|
658
|
-
|
|
534
|
+
this.pending.set(record.occurrenceId,record);
|
|
535
|
+
if(!record.retryRequired){
|
|
536
|
+
this.scheduleRecord(record,dueAt-timestamp);
|
|
537
|
+
}
|
|
538
|
+
this.offerDeveloperIncident(record.incident,record.occurrenceId);
|
|
659
539
|
}
|
|
660
540
|
|
|
661
541
|
this.persistLedger();
|
|
@@ -690,15 +570,11 @@ class Errors {
|
|
|
690
570
|
return this.isDeveloperMode();
|
|
691
571
|
}catch(error){
|
|
692
572
|
this.warn('Unable to read the developer-mode preference.',error);
|
|
693
|
-
return
|
|
573
|
+
return null;
|
|
694
574
|
}
|
|
695
575
|
}
|
|
696
576
|
|
|
697
|
-
|
|
698
|
-
if(!this.deferredDeveloperIncident){
|
|
699
|
-
this.deferredDeveloperIncident={ fingerprint,incident };
|
|
700
|
-
}
|
|
701
|
-
|
|
577
|
+
waitForDeveloperPreference(){
|
|
702
578
|
if(this.waitingForUser){
|
|
703
579
|
return;
|
|
704
580
|
}
|
|
@@ -714,306 +590,191 @@ class Errors {
|
|
|
714
590
|
onUserLoaded(){
|
|
715
591
|
this.#stopUserLoaded=null;
|
|
716
592
|
this.waitingForUser=false;
|
|
717
|
-
|
|
718
|
-
const deferred=this.deferredDeveloperIncident;
|
|
719
|
-
this.deferredDeveloperIncident=null;
|
|
720
|
-
|
|
721
|
-
if(deferred){
|
|
722
|
-
this.offerDeveloperIncident(
|
|
723
|
-
deferred.incident,
|
|
724
|
-
deferred.fingerprint,
|
|
725
|
-
false
|
|
726
|
-
);
|
|
727
|
-
}
|
|
593
|
+
this.drainDeveloperIncidents();
|
|
728
594
|
}
|
|
729
595
|
|
|
730
|
-
|
|
596
|
+
drainDeveloperIncidents(){
|
|
731
597
|
if(
|
|
732
598
|
this.destroyed
|
|
733
|
-
|| this.
|
|
734
|
-
|| this.
|
|
735
|
-
|| this.developerShownFingerprints.has(fingerprint)
|
|
599
|
+
|| this.developerPresentationActive
|
|
600
|
+
|| this.developerIncidentQueue.length===0
|
|
736
601
|
){
|
|
737
602
|
return false;
|
|
738
603
|
}
|
|
739
604
|
|
|
740
605
|
const developerMode=this.readDeveloperMode();
|
|
606
|
+
if(developerMode===null||developerMode===undefined){
|
|
607
|
+
this.waitForDeveloperPreference();
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
|
|
741
611
|
if(developerMode!==true){
|
|
742
|
-
|
|
743
|
-
this.deferDeveloperIncident(incident,fingerprint);
|
|
744
|
-
}
|
|
612
|
+
this.developerIncidentQueue.length=0;
|
|
745
613
|
return false;
|
|
746
614
|
}
|
|
747
615
|
|
|
748
|
-
this.
|
|
749
|
-
this.
|
|
616
|
+
const next=this.developerIncidentQueue.shift();
|
|
617
|
+
this.developerPresentationActive=true;
|
|
618
|
+
Promise.resolve()
|
|
619
|
+
.then(() => this.presentDeveloperIncident(
|
|
620
|
+
next.incident,
|
|
621
|
+
next.occurrenceId
|
|
622
|
+
))
|
|
623
|
+
.catch(error => {
|
|
624
|
+
this.warn('Developer error display failed.',error);
|
|
625
|
+
})
|
|
626
|
+
.finally(() => {
|
|
627
|
+
this.developerPresentationActive=false;
|
|
628
|
+
this.drainDeveloperIncidents();
|
|
629
|
+
});
|
|
750
630
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
this.developerUiDisabled=true;
|
|
757
|
-
this.warn('Developer error display failed and has been disabled for this session.',error);
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
offerDeveloperIncident(incident,occurrenceId){
|
|
635
|
+
if(this.destroyed){
|
|
758
636
|
return false;
|
|
759
637
|
}
|
|
760
638
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
error => {
|
|
766
|
-
this.developerModalActive=false;
|
|
767
|
-
this.developerUiDisabled=true;
|
|
768
|
-
this.warn('Developer error display failed and has been disabled for this session.',error);
|
|
769
|
-
}
|
|
770
|
-
);
|
|
639
|
+
const developerMode=this.readDeveloperMode();
|
|
640
|
+
if(developerMode===false){
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
771
643
|
|
|
644
|
+
this.developerIncidentQueue.push({ incident,occurrenceId });
|
|
645
|
+
if(developerMode===null||developerMode===undefined){
|
|
646
|
+
this.waitForDeveloperPreference();
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
this.drainDeveloperIncidents();
|
|
772
651
|
return true;
|
|
773
652
|
}
|
|
774
653
|
|
|
775
654
|
capture(incident){
|
|
776
|
-
if(this.destroyed
|
|
655
|
+
if(this.destroyed){
|
|
777
656
|
return false;
|
|
778
657
|
}
|
|
779
658
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
659
|
+
let occurrenceId=fingerprintIncident(incident);
|
|
660
|
+
while(this.pending.has(occurrenceId)){
|
|
661
|
+
occurrenceId=fingerprintIncident(incident);
|
|
783
662
|
}
|
|
784
|
-
|
|
785
663
|
const incidentKind=incident?.type==='unhandledrejection'
|
|
786
664
|
?'unhandled-promise-rejection'
|
|
787
665
|
:'browser-error';
|
|
788
666
|
const eventType=incidentKind==='unhandled-promise-rejection'
|
|
789
|
-
?
|
|
790
|
-
:
|
|
667
|
+
?ERROR_EVENT_TYPES.unhandledRejectionCaptured
|
|
668
|
+
:ERROR_EVENT_TYPES.browserErrorCaptured;
|
|
791
669
|
const code=incidentKind==='unhandled-promise-rejection'
|
|
792
|
-
?
|
|
793
|
-
:
|
|
670
|
+
?ERROR_EVENT_CODES.unhandledRejectionCaptured
|
|
671
|
+
:ERROR_EVENT_CODES.browserErrorCaptured;
|
|
794
672
|
const reason=incidentKind==='unhandled-promise-rejection'
|
|
795
|
-
?
|
|
796
|
-
:
|
|
673
|
+
?ERROR_REASONS.unhandledRejectionCaptured
|
|
674
|
+
:ERROR_REASONS.browserErrorCaptured;
|
|
797
675
|
this.#operationSequence+=1;
|
|
798
676
|
this.#events.dispatch(
|
|
799
677
|
eventType,
|
|
800
|
-
|
|
678
|
+
{id:occurrenceId,fingerprint:occurrenceId,code,kind:incidentKind,reason},
|
|
801
679
|
{
|
|
802
680
|
operationId:`global-error-handler-${this.#events.instanceId}-${this.#operationSequence.toString(36)}`,
|
|
803
|
-
publicDetail:
|
|
804
|
-
id:
|
|
681
|
+
publicDetail:{
|
|
682
|
+
id:occurrenceId,
|
|
683
|
+
fingerprint:occurrenceId,
|
|
805
684
|
code,
|
|
806
685
|
kind:incidentKind,
|
|
807
686
|
reason
|
|
808
|
-
}
|
|
687
|
+
}
|
|
809
688
|
}
|
|
810
689
|
);
|
|
811
690
|
|
|
812
|
-
this.offerDeveloperIncident(incident,
|
|
813
|
-
|
|
814
|
-
if(this.circuitOpen){
|
|
815
|
-
return false;
|
|
816
|
-
}
|
|
691
|
+
this.offerDeveloperIncident(incident,occurrenceId);
|
|
817
692
|
|
|
818
693
|
const timestamp=this.now();
|
|
819
|
-
const existing=this.pending.get(fingerprint);
|
|
820
|
-
if(existing){
|
|
821
|
-
existing.count++;
|
|
822
|
-
existing.lastSeen=timestamp;
|
|
823
|
-
if(existing.count===2){
|
|
824
|
-
return this.persistLedger();
|
|
825
|
-
}
|
|
826
|
-
return true;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
if(this.pending.size>=this.maxPendingIncidents){
|
|
830
|
-
const stormRecord=this.pending.values().next().value;
|
|
831
|
-
if(stormRecord){
|
|
832
|
-
const stormAlreadyDetected=stormRecord.errorStormDetected;
|
|
833
|
-
stormRecord.errorStormDetected=true;
|
|
834
|
-
stormRecord.lastSeen=timestamp;
|
|
835
|
-
stormRecord.uniqueIncidentCount=stormAlreadyDetected
|
|
836
|
-
? stormRecord.uniqueIncidentCount+1
|
|
837
|
-
: this.pending.size+1;
|
|
838
|
-
this.persistLedger();
|
|
839
|
-
}
|
|
840
|
-
return false;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
694
|
const record={
|
|
844
|
-
|
|
695
|
+
capturedAt:timestamp,
|
|
696
|
+
delivering:false,
|
|
845
697
|
dueAt:timestamp+this.delayMs,
|
|
846
|
-
errorStormDetected:false,
|
|
847
|
-
fingerprint,
|
|
848
|
-
firstSeen:timestamp,
|
|
849
698
|
incident,
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
699
|
+
occurrenceId,
|
|
700
|
+
retryRequired:false,
|
|
701
|
+
timer:null
|
|
853
702
|
};
|
|
854
703
|
|
|
855
|
-
this.pending.set(
|
|
856
|
-
// This is intentionally a fixed window from the first occurrence.
|
|
857
|
-
// Resetting the timer on every repeat could postpone a true loop forever.
|
|
858
|
-
if(!this.scheduleRecord(record,this.delayMs)){
|
|
859
|
-
return false;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
return this.persistLedger();
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
reserveDelivery(timestamp){
|
|
866
|
-
const cutoff=timestamp-this.rateWindowMs;
|
|
867
|
-
const recentReportTimestamps=this.reportTimestamps.filter(
|
|
868
|
-
reportTimestamp => reportTimestamp>=cutoff
|
|
869
|
-
);
|
|
870
|
-
|
|
871
|
-
if(
|
|
872
|
-
this.reportTimestamps.length>=this.maxReportsPerSession
|
|
873
|
-
|| recentReportTimestamps.length>=this.maxReportsPerWindow
|
|
874
|
-
){
|
|
875
|
-
this.openCircuit();
|
|
876
|
-
return { allowed:false,circuitOpened:true };
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
this.reportTimestamps.push(timestamp);
|
|
880
|
-
|
|
881
|
-
const circuitOpened=(
|
|
882
|
-
this.reportTimestamps.length>=this.maxReportsPerSession
|
|
883
|
-
|| recentReportTimestamps.length+1>=this.maxReportsPerWindow
|
|
884
|
-
);
|
|
885
|
-
|
|
886
|
-
if(circuitOpened){
|
|
887
|
-
this.openCircuit();
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
return { allowed:true,circuitOpened };
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
openCircuit(){
|
|
894
|
-
this.circuitOpen=true;
|
|
895
|
-
|
|
896
|
-
for(const record of this.pending.values()){
|
|
897
|
-
this.cancelTimer(record);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
this.pending.clear();
|
|
704
|
+
this.pending.set(occurrenceId,record);
|
|
901
705
|
this.persistLedger();
|
|
706
|
+
this.scheduleRecord(record,this.delayMs);
|
|
707
|
+
return true;
|
|
902
708
|
}
|
|
903
709
|
|
|
904
|
-
|
|
905
|
-
const record=this.pending.get(
|
|
906
|
-
if(!record){
|
|
907
|
-
return;
|
|
710
|
+
flushOccurrence(occurrenceId){
|
|
711
|
+
const record=this.pending.get(occurrenceId);
|
|
712
|
+
if(!record||record.delivering){
|
|
713
|
+
return false;
|
|
908
714
|
}
|
|
909
715
|
|
|
910
|
-
this.pending.delete(fingerprint);
|
|
911
716
|
this.cancelTimer(record);
|
|
912
|
-
|
|
913
|
-
if(this.destroyed||this.circuitOpen||this.reported.has(fingerprint)){
|
|
717
|
+
if(this.destroyed){
|
|
914
718
|
this.persistLedger();
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
// Suppress before performing any fallible notification work. A failed
|
|
919
|
-
// mail request is an attempted report and must never be retried recursively.
|
|
920
|
-
this.reported.add(fingerprint);
|
|
921
|
-
const reservation=this.reserveDelivery(this.now());
|
|
922
|
-
if(record.errorStormDetected&&reservation.allowed&&!reservation.circuitOpened){
|
|
923
|
-
reservation.circuitOpened=true;
|
|
924
|
-
this.openCircuit();
|
|
925
|
-
}
|
|
926
|
-
const persisted=this.persistLedger();
|
|
927
|
-
|
|
928
|
-
if(!reservation.allowed||!persisted){
|
|
929
|
-
return;
|
|
719
|
+
return false;
|
|
930
720
|
}
|
|
931
721
|
|
|
722
|
+
record.delivering=true;
|
|
932
723
|
this.deliveryQueue=this.deliveryQueue
|
|
933
|
-
.then(() =>
|
|
724
|
+
.then(async () => {
|
|
725
|
+
if(this.destroyed){
|
|
726
|
+
record.retryRequired=true;
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
await this.deliver(record);
|
|
730
|
+
return true;
|
|
731
|
+
})
|
|
732
|
+
.then(delivered => {
|
|
733
|
+
if(!delivered){
|
|
734
|
+
record.delivering=false;
|
|
735
|
+
this.persistLedger();
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if(this.pending.get(occurrenceId)===record){
|
|
739
|
+
this.pending.delete(occurrenceId);
|
|
740
|
+
}
|
|
741
|
+
record.delivering=false;
|
|
742
|
+
this.persistLedger();
|
|
743
|
+
})
|
|
934
744
|
.catch(error => {
|
|
935
|
-
|
|
745
|
+
record.delivering=false;
|
|
746
|
+
record.retryRequired=true;
|
|
747
|
+
this.warn('Error notification failed; delivery remains pending for retry.',error);
|
|
748
|
+
this.persistLedger();
|
|
936
749
|
});
|
|
750
|
+
return true;
|
|
937
751
|
}
|
|
938
752
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
753
|
+
flushFingerprint(fingerprint){
|
|
754
|
+
return this.flushOccurrence(fingerprint);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
buildNotification(record){
|
|
758
|
+
const subject=record.incident?.type==='unhandledrejection'
|
|
944
759
|
? 'ARCANE JS UNHANDLED REJECTION'
|
|
945
760
|
: 'ARCANE JS ERROR';
|
|
946
|
-
|
|
947
|
-
let subject=baseSubject;
|
|
948
|
-
if(errorStormDetected){
|
|
949
|
-
subject=`${baseSubject} - ERROR STORM DETECTED`;
|
|
950
|
-
}else if(loopDetected){
|
|
951
|
-
subject=`${baseSubject} - LOOP DETECTED`;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
761
|
const payload={
|
|
955
762
|
...record.incident,
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
first_seen_at:safeIso(record.firstSeen),
|
|
959
|
-
last_seen_at:safeIso(record.lastSeen),
|
|
960
|
-
observation_window_ms:this.delayMs,
|
|
961
|
-
loop_detected:loopDetected,
|
|
962
|
-
error_storm_detected:errorStormDetected,
|
|
963
|
-
unique_incident_count:record.uniqueIncidentCount,
|
|
964
|
-
matching_notifications_suppressed:true,
|
|
965
|
-
loop_notice:loopDetected
|
|
966
|
-
? `This error repeated ${record.count} times during the ${this.delayMs}ms observation window. Further matching notifications are suppressed for this browser session.`
|
|
967
|
-
: 'Further occurrences of this same error are suppressed for this browser session.',
|
|
968
|
-
circuit_breaker_notice:errorStormDetected
|
|
969
|
-
? 'The global notification limit was reached. Further error emails are suppressed for this browser session to stop a possible varying error loop.'
|
|
970
|
-
: null,
|
|
763
|
+
captured_at:safeIso(record.capturedAt),
|
|
764
|
+
occurrence_id:record.occurrenceId
|
|
971
765
|
};
|
|
972
766
|
|
|
973
767
|
return { payload,subject };
|
|
974
768
|
}
|
|
975
769
|
|
|
976
|
-
async deliver(record
|
|
977
|
-
const { payload,subject }=this.buildNotification(record
|
|
978
|
-
|
|
979
|
-
const delivery=Promise.resolve().then(() => {
|
|
980
|
-
// Only suppress events emitted synchronously by the adapter call.
|
|
981
|
-
// Its returned promise is observed below, so unrelated errors that
|
|
982
|
-
// occur while network delivery is pending must still be captured.
|
|
983
|
-
this.invokingMail=true;
|
|
984
|
-
try{
|
|
985
|
-
return this.sendMail([],subject,payload,MESSAGE_STYLE,'error');
|
|
986
|
-
}finally{
|
|
987
|
-
this.invokingMail=false;
|
|
988
|
-
}
|
|
989
|
-
});
|
|
990
|
-
|
|
991
|
-
try{
|
|
992
|
-
await Promise.race([
|
|
993
|
-
delivery,
|
|
994
|
-
new Promise((resolve,reject) => {
|
|
995
|
-
timeout=this.deliverySchedule(
|
|
996
|
-
() => reject(new Error('Error notification timed out')),
|
|
997
|
-
this.deliveryTimeoutMs
|
|
998
|
-
);
|
|
999
|
-
}),
|
|
1000
|
-
]);
|
|
1001
|
-
}catch(error){
|
|
1002
|
-
this.warn('Error notification failed; no retry will be attempted.',error);
|
|
1003
|
-
}finally{
|
|
1004
|
-
if(timeout!==null){
|
|
1005
|
-
try{
|
|
1006
|
-
this.deliveryCancel(timeout);
|
|
1007
|
-
}catch(error){
|
|
1008
|
-
this.warn('Unable to cancel the notification timeout.',error);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
770
|
+
async deliver(record){
|
|
771
|
+
const { payload,subject }=this.buildNotification(record);
|
|
772
|
+
await this.sendMail([],subject,payload,MESSAGE_STYLE,'error');
|
|
1012
773
|
}
|
|
1013
774
|
|
|
1014
775
|
async flush(){
|
|
1015
|
-
for(const
|
|
1016
|
-
this.
|
|
776
|
+
for(const occurrenceId of [...this.pending.keys()]){
|
|
777
|
+
this.flushOccurrence(occurrenceId);
|
|
1017
778
|
}
|
|
1018
779
|
|
|
1019
780
|
await this.whenIdle();
|
|
@@ -1034,7 +795,7 @@ class Errors {
|
|
|
1034
795
|
this.#stopUserLoaded?.();
|
|
1035
796
|
this.#stopUserLoaded=null;
|
|
1036
797
|
this.waitingForUser=false;
|
|
1037
|
-
this.
|
|
798
|
+
this.developerIncidentQueue.length=0;
|
|
1038
799
|
|
|
1039
800
|
try{
|
|
1040
801
|
const modal=this.target.document?.querySelector?.(
|
|
@@ -1061,7 +822,6 @@ class Errors {
|
|
|
1061
822
|
this.cancelTimer(record);
|
|
1062
823
|
}
|
|
1063
824
|
|
|
1064
|
-
this.pending.clear();
|
|
1065
825
|
this.persistLedger();
|
|
1066
826
|
|
|
1067
827
|
if(this.target.errors===this){
|