arcane-os 0.1.0-dev.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 +154 -0
- package/COMMERCIAL-LICENSE.md +13 -0
- package/LICENSE +661 -0
- package/NOTICE +70 -0
- package/README.md +448 -0
- package/bin/arcane-test.mjs +741 -0
- package/bin/arcane.mjs +5 -0
- package/docs/architecture.md +234 -0
- package/docs/compatibility.md +36 -0
- package/docs/event-manager.md +166 -0
- package/docs/platform-targets.md +108 -0
- package/docs/publishing.md +203 -0
- package/docs/reference/README.md +117 -0
- package/docs/reference/arcane-ollama.md +288 -0
- package/docs/reference/availability-and-normalization.md +123 -0
- package/docs/reference/behavioral-testing.md +86 -0
- package/docs/reference/cli.md +569 -0
- package/docs/reference/core/README.md +62 -0
- package/docs/reference/core/arcane-ai-contracts.md +873 -0
- package/docs/reference/core/arcane-api.md +601 -0
- package/docs/reference/core/arcane-entities.md +65 -0
- package/docs/reference/core/arcane-events.md +134 -0
- package/docs/reference/core/ollama-module.md +181 -0
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
- package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
- package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
- package/docs/reference/event-manager.md +957 -0
- package/docs/reference/inventory/package-api.json +2632 -0
- package/docs/reference/inventory/runtime-components.json +934 -0
- package/docs/reference/inventory/runtime-entities.json +26 -0
- package/docs/reference/inventory/runtime-modules.json +1249 -0
- package/docs/reference/protocols.md +242 -0
- package/docs/reference/runtime-components.md +1098 -0
- package/docs/reference/runtime-entities.md +303 -0
- package/docs/reference/runtime-modules.md +2010 -0
- package/docs/reference/sdk-api.md +4901 -0
- package/docs/roadmap.md +79 -0
- package/docs/work-amplification.md +124 -0
- package/node_modules/event-pubsub/CHANGELOG.md +55 -0
- package/node_modules/event-pubsub/MIGRATION.md +70 -0
- package/node_modules/event-pubsub/README.md +363 -0
- package/node_modules/event-pubsub/SECURITY.md +37 -0
- package/node_modules/event-pubsub/index.js +141 -0
- package/node_modules/event-pubsub/licence +21 -0
- package/node_modules/event-pubsub/package.json +59 -0
- package/node_modules/strong-type/README.md +408 -0
- package/node_modules/strong-type/assets/strong-type-header.png +0 -0
- package/node_modules/strong-type/index.js +1151 -0
- package/node_modules/strong-type/licence +21 -0
- package/node_modules/strong-type/node.js +125 -0
- package/node_modules/strong-type/package.json +61 -0
- package/package.json +95 -0
- package/runtime/ARCANE_RUNTIME_RELEASE.json +791 -0
- package/runtime/arcane/components/app-bar.html +468 -0
- package/runtime/arcane/components/assistant-panel.html +715 -0
- package/runtime/arcane/components/calculator.html +8 -0
- package/runtime/arcane/components/chart.html +655 -0
- package/runtime/arcane/components/chat.html +1225 -0
- package/runtime/arcane/components/conversation-view.html +13 -0
- package/runtime/arcane/components/dashboard-config.html +341 -0
- package/runtime/arcane/components/data-maintenance.html +112 -0
- package/runtime/arcane/components/data-view.html +92 -0
- package/runtime/arcane/components/directory-picker.html +197 -0
- package/runtime/arcane/components/document-inspector.html +252 -0
- package/runtime/arcane/components/file-drop.html +264 -0
- package/runtime/arcane/components/file-inspector.html +293 -0
- package/runtime/arcane/components/file-manager.html +1715 -0
- package/runtime/arcane/components/header.html +142 -0
- package/runtime/arcane/components/integration-settings.html +14 -0
- package/runtime/arcane/components/local-ai-status.html +360 -0
- package/runtime/arcane/components/markdown-document.html +1048 -0
- package/runtime/arcane/components/markdown-editor.html +360 -0
- package/runtime/arcane/components/media-embed.html +8 -0
- package/runtime/arcane/components/modal.html +402 -0
- package/runtime/arcane/components/output-panel.html +259 -0
- package/runtime/arcane/components/preferences-form.html +135 -0
- package/runtime/arcane/components/record-timeline.html +105 -0
- package/runtime/arcane/components/relationship-board.html +116 -0
- package/runtime/arcane/components/screen-capture.html +8 -0
- package/runtime/arcane/components/source-code-viewer.html +441 -0
- package/runtime/arcane/components/source-explanation.html +124 -0
- package/runtime/arcane/components/speech.html +365 -0
- package/runtime/arcane/components/summary-strip.html +177 -0
- package/runtime/arcane/components/table.html +77 -0
- package/runtime/arcane/components/task-progress.html +282 -0
- package/runtime/arcane/components/terminal-workspace.html +65 -0
- package/runtime/arcane/components/theme-editor.html +41 -0
- package/runtime/arcane/components/theme-switcher.html +46 -0
- package/runtime/arcane/components/unified-inbox.html +20 -0
- package/runtime/arcane/components/voice-transcription.html +476 -0
- package/runtime/arcane/components/weather-widget.html +8 -0
- package/runtime/arcane/components/web-navigator.html +239 -0
- package/runtime/arcane/css/communications.css +1 -0
- package/runtime/arcane/css/dashboard-config.css +45 -0
- package/runtime/arcane/css/document-site.css +981 -0
- package/runtime/arcane/css/layout.css +438 -0
- package/runtime/arcane/css/primitives.css +321 -0
- package/runtime/arcane/css/theme.css +112 -0
- package/runtime/arcane/css/utility-workspace.css +1 -0
- package/runtime/arcane/entities/ApiModelRecord.js +20 -0
- package/runtime/arcane/entities/Calculation.js +13 -0
- package/runtime/arcane/entities/Chat.js +581 -0
- package/runtime/arcane/entities/CommunicationMessage.js +29 -0
- package/runtime/arcane/entities/CommunicationThread.js +21 -0
- package/runtime/arcane/entities/Document.js +10 -0
- package/runtime/arcane/entities/File.js +143 -0
- package/runtime/arcane/entities/Image.js +135 -0
- package/runtime/arcane/entities/IntentEnvelope.js +834 -0
- package/runtime/arcane/entities/Preference.js +83 -0
- package/runtime/arcane/entities/TWiNPolicyDecision.js +1092 -0
- package/runtime/arcane/entities/TerminalSession.js +46 -0
- package/runtime/arcane/entities/Theme.js +107 -0
- package/runtime/arcane/entities/User.js +1046 -0
- package/runtime/arcane/entities/Weather.js +23 -0
- package/runtime/arcane/img/arcane-os-everywhere.png +0 -0
- package/runtime/arcane/img/arrow-left.png +0 -0
- package/runtime/arcane/img/arrow-right.png +0 -0
- package/runtime/arcane/img/doc.svg +5 -0
- package/runtime/arcane/img/folder.svg +4 -0
- package/runtime/arcane/img/image.svg +5 -0
- package/runtime/arcane/img/refresh.png +0 -0
- package/runtime/arcane/img/send.svg +9 -0
- package/runtime/arcane/img/trash.svg +5 -0
- package/runtime/arcane/img/upload.svg +5 -0
- package/runtime/arcane/modules/AI.js +2048 -0
- package/runtime/arcane/modules/AIPreferenceRuntime.js +32 -0
- package/runtime/arcane/modules/AIPreferenceTuple.js +92 -0
- package/runtime/arcane/modules/AIResponseLength.js +42 -0
- package/runtime/arcane/modules/AIResponseURLPolicy.js +626 -0
- package/runtime/arcane/modules/AnsiText.js +53 -0
- package/runtime/arcane/modules/ApiModelDatabase.js +25 -0
- package/runtime/arcane/modules/AppDataScope.js +245 -0
- package/runtime/arcane/modules/AppearancePreferences.js +28 -0
- package/runtime/arcane/modules/ArcaneCommunicationBridge.js +22 -0
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +135 -0
- package/runtime/arcane/modules/ArcaneNetworkPolicy.js +255 -0
- package/runtime/arcane/modules/AsyncBoundary.js +161 -0
- package/runtime/arcane/modules/BrowserTestSuite.js +326 -0
- package/runtime/arcane/modules/CalculatorEngine.js +20 -0
- package/runtime/arcane/modules/CaseEvidenceIndexer.js +134 -0
- package/runtime/arcane/modules/ChartLibrary.js +35 -0
- package/runtime/arcane/modules/ChatRecords.js +13 -0
- package/runtime/arcane/modules/CommunicationAppController.js +43 -0
- package/runtime/arcane/modules/CommunicationHub.js +16 -0
- package/runtime/arcane/modules/CommunicationPreferences.js +13 -0
- package/runtime/arcane/modules/CommunicationProviderRegistry.js +16 -0
- package/runtime/arcane/modules/ComponentContracts.js +586 -0
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +288 -0
- package/runtime/arcane/modules/ConversationActionItems.js +488 -0
- package/runtime/arcane/modules/ConversationClosingReport.js +274 -0
- package/runtime/arcane/modules/ConversationTimebox.js +527 -0
- package/runtime/arcane/modules/CoreLocalModelCatalog.js +255 -0
- package/runtime/arcane/modules/DBLS.js +171 -0
- package/runtime/arcane/modules/DBOPFS.js +1154 -0
- package/runtime/arcane/modules/DBOPFSWorker.js +116 -0
- package/runtime/arcane/modules/DataMaintenance.js +91 -0
- package/runtime/arcane/modules/DevelopmentWorkspace.js +74 -0
- package/runtime/arcane/modules/DirectoryPicker.js +109 -0
- package/runtime/arcane/modules/DocumentNavigation.js +223 -0
- package/runtime/arcane/modules/Errors.js +1025 -0
- package/runtime/arcane/modules/GifEncoder.js +29 -0
- package/runtime/arcane/modules/HTMLImport.js +114 -0
- package/runtime/arcane/modules/InMemoryCommunicationProvider.js +14 -0
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +275 -0
- package/runtime/arcane/modules/LocalAIReadiness.js +870 -0
- package/runtime/arcane/modules/LocalAIReadinessController.js +156 -0
- package/runtime/arcane/modules/MD.js +111 -0
- package/runtime/arcane/modules/Mail.js +352 -0
- package/runtime/arcane/modules/MailTransport.mjs +180 -0
- package/runtime/arcane/modules/Marked.min.js +71 -0
- package/runtime/arcane/modules/MemoryRecords.js +44 -0
- package/runtime/arcane/modules/MessageAdvisory.js +38 -0
- package/runtime/arcane/modules/ModelDefinition.js +189 -0
- package/runtime/arcane/modules/Ollama.js +74 -0
- package/runtime/arcane/modules/OllamaModelIdentifier.js +21 -0
- package/runtime/arcane/modules/OllamaSettings.js +24 -0
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +14 -0
- package/runtime/arcane/modules/PreferenceStore.js +109 -0
- package/runtime/arcane/modules/QRCode.min.js +1 -0
- package/runtime/arcane/modules/Questionnaire.js +61 -0
- package/runtime/arcane/modules/RecordLinkIndex.js +24 -0
- package/runtime/arcane/modules/RecordPassageIndex.js +222 -0
- package/runtime/arcane/modules/RecordReviewStore.js +94 -0
- package/runtime/arcane/modules/RevocableProjectionLedger.js +1623 -0
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +37 -0
- package/runtime/arcane/modules/ScamRiskPolicy.js +62 -0
- package/runtime/arcane/modules/ScopedOPFSCache.js +183 -0
- package/runtime/arcane/modules/ScreenCapture.js +20 -0
- package/runtime/arcane/modules/SpeechPlayback.js +581 -0
- package/runtime/arcane/modules/StaticDocumentCatalog.js +1248 -0
- package/runtime/arcane/modules/SystemAppearance.js +20 -0
- package/runtime/arcane/modules/SystemPlatformPresentation.js +51 -0
- package/runtime/arcane/modules/SystemToolRegistry.js +28 -0
- package/runtime/arcane/modules/TerminalClient.js +52 -0
- package/runtime/arcane/modules/TerminalCommandRegistry.js +50 -0
- package/runtime/arcane/modules/ThemeBootstrap.js +26 -0
- package/runtime/arcane/modules/ThemeManager.js +131 -0
- package/runtime/arcane/modules/TimeGuard.js +149 -0
- package/runtime/arcane/modules/ToolCallRouter.js +83 -0
- package/runtime/arcane/modules/WaitForComponent.js +102 -0
- package/runtime/arcane/modules/YouTubeMedia.js +16 -0
- package/runtime/arcane/modules/uPlot.LICENSE.txt +21 -0
- package/runtime/arcane/modules/uPlot.iife.min.js +2 -0
- package/runtime/arcane/modules/uPlot.min.css +1 -0
- package/runtime/arcane/security/arcane-network-policy.json +6 -0
- package/runtime/strong-type/index.js +352 -0
- package/runtime/strong-type/licence +21 -0
- package/runtime/strong-type/package.json +45 -0
- package/schemas/arcane-app-bundle.schema.json +125 -0
- package/schemas/arcane-app.schema.json +329 -0
- package/schemas/arcane-lock.schema.json +86 -0
- package/schemas/arcane-package.schema.json +224 -0
- package/schemas/cli-event.schema.json +122 -0
- package/schemas/event-stack.schema.json +152 -0
- package/schemas/native-build-plan.schema.json +176 -0
- package/schemas/target-adapter.schema.json +117 -0
- package/src/app-descriptor.mjs +500 -0
- package/src/cli/main.mjs +561 -0
- package/src/constants.mjs +31 -0
- package/src/dev-server.mjs +718 -0
- package/src/doctor.mjs +315 -0
- package/src/dom-event-instrumentation.mjs +594 -0
- package/src/errors.mjs +75 -0
- package/src/event-manager.mjs +1342 -0
- package/src/event-queue.mjs +138 -0
- package/src/events.mjs +219 -0
- package/src/index.mjs +177 -0
- package/src/integrated-provider-loader.mjs +432 -0
- package/src/native-plan.mjs +698 -0
- package/src/native-provider-loader.mjs +1126 -0
- package/src/packager/core.mjs +2691 -0
- package/src/process.mjs +353 -0
- package/src/release-bundle.mjs +2523 -0
- package/src/repository.mjs +90 -0
- package/src/runtime.mjs +452 -0
- package/src/scaffold.mjs +380 -0
- package/src/targets/index.mjs +436 -0
- package/src/templates/assets/app-icon.png +0 -0
- package/src/templates/workspace-template.mjs +388 -0
- package/src/testing-loader.mjs +9 -0
- package/src/testing.mjs +427 -0
- package/src/toolchain.mjs +1335 -0
- package/src/update-check.mjs +307 -0
- package/src/workspace.mjs +449 -0
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
import waitForComponent from './WaitForComponent.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_DELAY_MS=2_000;
|
|
4
|
+
const DEFAULT_MAX_REPORTS_PER_SESSION=10;
|
|
5
|
+
const DEFAULT_MAX_REPORTS_PER_WINDOW=3;
|
|
6
|
+
const DEFAULT_MAX_PENDING_INCIDENTS=25;
|
|
7
|
+
const DEFAULT_RATE_WINDOW_MS=60_000;
|
|
8
|
+
const LEDGER_STORAGE_KEY='arcane-global-errors-v1';
|
|
9
|
+
const MAX_DETAIL_LENGTH=8_000;
|
|
10
|
+
const HANDLER_MARKER=Symbol.for('arcane.global-errors.handler');
|
|
11
|
+
const DEVELOPER_MODAL_HREF=new URL('../components/modal.html?v=13',import.meta.url).href;
|
|
12
|
+
|
|
13
|
+
const MESSAGE_STYLE=[
|
|
14
|
+
'Write a simple plain-text email showing the error and, when the available details support it, a possible solution.',
|
|
15
|
+
'If loop_detected or error_storm_detected is true, put that warning first and clearly state that further notifications have been suppressed.',
|
|
16
|
+
'Do not add facts that are not present in the report data.',
|
|
17
|
+
].join(' ');
|
|
18
|
+
|
|
19
|
+
function safeText(value,fallback=''){
|
|
20
|
+
if(value===undefined||value===null){
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try{
|
|
25
|
+
const text=typeof value==='string' ? value:String(value);
|
|
26
|
+
return text.slice(0,MAX_DETAIL_LENGTH)||fallback;
|
|
27
|
+
}catch{
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function safeNumber(value){
|
|
33
|
+
const number=Number(value);
|
|
34
|
+
return Number.isFinite(number) ? number:null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function safeIso(timestamp){
|
|
38
|
+
try{
|
|
39
|
+
return new Date(timestamp).toISOString();
|
|
40
|
+
}catch{
|
|
41
|
+
return new Date(0).toISOString();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resourceUrlFrom(event,target){
|
|
46
|
+
const resource=event?.target;
|
|
47
|
+
if(!resource||resource===target){
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return safeText(resource.currentSrc||resource.src||resource.href);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Normalize a browser ErrorEvent or capture-phase resource error.
|
|
56
|
+
*
|
|
57
|
+
* @param {ErrorEvent|Event|Object} event
|
|
58
|
+
* @param {Window|Object} target
|
|
59
|
+
* @returns {Object}
|
|
60
|
+
*/
|
|
61
|
+
export function normalizeErrorEvent(event={},target=globalThis.window){
|
|
62
|
+
const error=event?.error;
|
|
63
|
+
const resourceUrl=resourceUrlFrom(event,target);
|
|
64
|
+
const fallbackPath=safeText(target?.location?.pathname||target?.location?.href);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
type:'error',
|
|
68
|
+
message:safeText(
|
|
69
|
+
event?.message||error?.message,
|
|
70
|
+
resourceUrl ? 'Resource failed to load':'Unknown global error'
|
|
71
|
+
),
|
|
72
|
+
name:safeText(
|
|
73
|
+
error?.name,
|
|
74
|
+
resourceUrl ? 'ResourceLoadError':'Error'
|
|
75
|
+
),
|
|
76
|
+
stack:safeText(error?.stack)||null,
|
|
77
|
+
filename:safeText(event?.filename||resourceUrl||fallbackPath)||null,
|
|
78
|
+
lineno:safeNumber(event?.lineno),
|
|
79
|
+
colno:safeNumber(event?.colno),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Normalize an unhandled Promise rejection, including non-Error reasons.
|
|
85
|
+
*
|
|
86
|
+
* @param {PromiseRejectionEvent|Object} event
|
|
87
|
+
* @param {Window|Object} target
|
|
88
|
+
* @returns {Object}
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeRejectionEvent(event={},target=globalThis.window){
|
|
91
|
+
const reason=event?.reason;
|
|
92
|
+
const reasonIsObject=reason!==null
|
|
93
|
+
&& (typeof reason==='object'||typeof reason==='function');
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
type:'unhandledrejection',
|
|
97
|
+
message:safeText(
|
|
98
|
+
reasonIsObject ? reason?.message:reason,
|
|
99
|
+
'Unhandled promise rejection'
|
|
100
|
+
),
|
|
101
|
+
name:safeText(
|
|
102
|
+
reasonIsObject ? reason?.name:'',
|
|
103
|
+
'UnhandledRejection'
|
|
104
|
+
),
|
|
105
|
+
stack:safeText(reasonIsObject ? reason?.stack:'')||null,
|
|
106
|
+
filename:safeText(target?.location?.pathname||target?.location?.href)||null,
|
|
107
|
+
lineno:null,
|
|
108
|
+
colno:null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeFingerprintPart(value){
|
|
113
|
+
return safeText(value)
|
|
114
|
+
.trim()
|
|
115
|
+
.replace(/\s+/g,' ')
|
|
116
|
+
.slice(0,2_000);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeVolatileFingerprintPart(value){
|
|
120
|
+
return normalizeFingerprintPart(value)
|
|
121
|
+
.replace(/[?#][^\s)]+/g,'')
|
|
122
|
+
.replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi,'<uuid>')
|
|
123
|
+
.replace(/\b[0-9a-f]{16,}\b/gi,'<hex>')
|
|
124
|
+
.replace(/\b\d+(?:\.\d+)?\b/g,'<number>');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeStackFingerprint(value,name,message){
|
|
128
|
+
const stack=safeText(value).trim();
|
|
129
|
+
if(!stack){
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const lines=stack.split(/\r?\n/);
|
|
134
|
+
const firstLine=lines[0].trim();
|
|
135
|
+
const errorName=safeText(name).trim();
|
|
136
|
+
const errorMessage=safeText(message).trim();
|
|
137
|
+
const hasMessageHeader=lines.length>1&&(
|
|
138
|
+
firstLine===errorMessage
|
|
139
|
+
|| firstLine===`${errorName}: ${errorMessage}`
|
|
140
|
+
|| (errorName&&firstLine.startsWith(`${errorName}:`))
|
|
141
|
+
|| /^(?:Error|[A-Za-z_$][\w.$]*(?:Error|Exception)):(?:\s|$)/.test(firstLine)
|
|
142
|
+
);
|
|
143
|
+
const frames=hasMessageHeader ? lines.slice(1):lines;
|
|
144
|
+
|
|
145
|
+
return normalizeFingerprintPart(frames.join('\n'))
|
|
146
|
+
.replace(/[?#][^\s)]+/g,'')
|
|
147
|
+
.replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi,'<uuid>')
|
|
148
|
+
.replace(/\b[0-9a-f]{16,}\b/gi,'<hex>');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function hashText(value){
|
|
152
|
+
let first=0x811c9dc5;
|
|
153
|
+
let second=0x9e3779b9;
|
|
154
|
+
|
|
155
|
+
for(let index=0;index<value.length;index++){
|
|
156
|
+
const code=value.charCodeAt(index);
|
|
157
|
+
first=Math.imul(first^code,0x01000193);
|
|
158
|
+
second=Math.imul(second^code,0x85ebca6b);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return [first,second]
|
|
162
|
+
.map(hash => (hash>>>0).toString(16).padStart(8,'0'))
|
|
163
|
+
.join('');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Build a stable call-site signature. When a precise source location is
|
|
168
|
+
* available, the human-readable message is deliberately excluded so loops
|
|
169
|
+
* with changing counters, names, or timestamps still produce one report.
|
|
170
|
+
*
|
|
171
|
+
* @param {Object} incident
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
export function fingerprintIncident(incident){
|
|
175
|
+
const stackFingerprint=normalizeStackFingerprint(
|
|
176
|
+
incident?.stack,
|
|
177
|
+
incident?.name,
|
|
178
|
+
incident?.message
|
|
179
|
+
);
|
|
180
|
+
const hasPreciseSource=(
|
|
181
|
+
Number.isFinite(incident?.lineno)
|
|
182
|
+
|| Number.isFinite(incident?.colno)
|
|
183
|
+
|| Boolean(stackFingerprint)
|
|
184
|
+
);
|
|
185
|
+
const signature=[
|
|
186
|
+
normalizeFingerprintPart(incident?.type),
|
|
187
|
+
normalizeFingerprintPart(incident?.name),
|
|
188
|
+
hasPreciseSource
|
|
189
|
+
? ''
|
|
190
|
+
: normalizeVolatileFingerprintPart(incident?.message),
|
|
191
|
+
normalizeFingerprintPart(incident?.filename).replace(/[?#].*$/,''),
|
|
192
|
+
normalizeFingerprintPart(incident?.lineno),
|
|
193
|
+
normalizeFingerprintPart(incident?.colno),
|
|
194
|
+
stackFingerprint,
|
|
195
|
+
].join('|');
|
|
196
|
+
|
|
197
|
+
return `error-${hashText(signature)}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function defaultStorage(target){
|
|
201
|
+
try{
|
|
202
|
+
return target?.sessionStorage||null;
|
|
203
|
+
}catch{
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function sendWithWindowMail(target,...args){
|
|
209
|
+
if(typeof target?.mail?.send!=='function'){
|
|
210
|
+
await import('./Mail.js');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if(typeof target?.mail?.send!=='function'){
|
|
214
|
+
throw new Error('Mail notification service is unavailable');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return target.mail.send(...args);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function defaultDeveloperModeStatus(target){
|
|
221
|
+
try{
|
|
222
|
+
if(target?.user?.ready!==true){
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return target.user.developer===true;
|
|
227
|
+
}catch{
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function appendDeveloperDetail(document,list,label,value){
|
|
233
|
+
if(value===null||value===undefined||value===''){
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const term=document.createElement('dt');
|
|
238
|
+
const description=document.createElement('dd');
|
|
239
|
+
|
|
240
|
+
term.textContent=label;
|
|
241
|
+
term.style.fontWeight='bold';
|
|
242
|
+
description.textContent=safeText(value);
|
|
243
|
+
description.style.margin='0 0 0.75em';
|
|
244
|
+
description.style.overflowWrap='anywhere';
|
|
245
|
+
list.append(term,description);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildDeveloperIncidentContent(document,incident,fingerprint){
|
|
249
|
+
const content=document.createElement('section');
|
|
250
|
+
const heading=document.createElement('h2');
|
|
251
|
+
const introduction=document.createElement('p');
|
|
252
|
+
const details=document.createElement('dl');
|
|
253
|
+
const source=[
|
|
254
|
+
safeText(incident?.filename),
|
|
255
|
+
Number.isFinite(incident?.lineno) ? incident.lineno:'',
|
|
256
|
+
Number.isFinite(incident?.colno) ? incident.colno:'',
|
|
257
|
+
].filter(value => value!=='').join(':');
|
|
258
|
+
|
|
259
|
+
content.className='developer-error-content';
|
|
260
|
+
heading.textContent='Application Error';
|
|
261
|
+
introduction.textContent='Developer mode captured this application error.';
|
|
262
|
+
details.style.margin='1.5em 0';
|
|
263
|
+
|
|
264
|
+
appendDeveloperDetail(document,details,'Type',incident?.type);
|
|
265
|
+
appendDeveloperDetail(document,details,'Name',incident?.name);
|
|
266
|
+
appendDeveloperDetail(document,details,'Message',incident?.message);
|
|
267
|
+
appendDeveloperDetail(document,details,'Source',source);
|
|
268
|
+
appendDeveloperDetail(document,details,'Fingerprint',fingerprint);
|
|
269
|
+
|
|
270
|
+
content.append(heading,introduction,details);
|
|
271
|
+
|
|
272
|
+
if(incident?.stack){
|
|
273
|
+
const stackHeading=document.createElement('h3');
|
|
274
|
+
const stack=document.createElement('pre');
|
|
275
|
+
|
|
276
|
+
stackHeading.textContent='Stack';
|
|
277
|
+
stack.textContent=safeText(incident.stack);
|
|
278
|
+
stack.style.overflowWrap='anywhere';
|
|
279
|
+
stack.style.whiteSpace='pre-wrap';
|
|
280
|
+
content.append(stackHeading,stack);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return content;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function ensureHTMLImport(target){
|
|
287
|
+
const registry=target?.customElements||globalThis.customElements;
|
|
288
|
+
|
|
289
|
+
if(!registry?.get){
|
|
290
|
+
throw new Error('Custom elements are unavailable');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if(registry.get('html-import')){
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try{
|
|
298
|
+
await import('./HTMLImport.js');
|
|
299
|
+
}catch(error){
|
|
300
|
+
if(!registry.get('html-import')){
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if(!registry.get('html-import')){
|
|
306
|
+
throw new Error('The html-import component is unavailable');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function presentDeveloperIncidentModal(target,incident,fingerprint){
|
|
311
|
+
const document=target?.document;
|
|
312
|
+
const container=document?.body||document?.documentElement;
|
|
313
|
+
|
|
314
|
+
if(!document?.createElement||!container?.append){
|
|
315
|
+
throw new Error('The document is not ready for an error modal');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
await ensureHTMLImport(target);
|
|
319
|
+
|
|
320
|
+
const modal=document.createElement('html-import');
|
|
321
|
+
const content=buildDeveloperIncidentContent(document,incident,fingerprint);
|
|
322
|
+
|
|
323
|
+
modal.className='modal developer-error-modal';
|
|
324
|
+
modal.setAttribute('aria-label','Application error');
|
|
325
|
+
modal.setAttribute('data-global-error-modal',fingerprint);
|
|
326
|
+
modal.setAttribute('data-once','');
|
|
327
|
+
modal.setAttribute('href',DEVELOPER_MODAL_HREF);
|
|
328
|
+
|
|
329
|
+
let resolveClosed;
|
|
330
|
+
const closed=new Promise(resolve => {
|
|
331
|
+
resolveClosed=resolve;
|
|
332
|
+
});
|
|
333
|
+
const finish=() => {
|
|
334
|
+
target.removeEventListener?.('pagehide',onPageHide);
|
|
335
|
+
resolveClosed();
|
|
336
|
+
};
|
|
337
|
+
const onPageHide=() => {
|
|
338
|
+
try{
|
|
339
|
+
if(typeof modal.close==='function'){
|
|
340
|
+
modal.close(undefined,true);
|
|
341
|
+
}else{
|
|
342
|
+
modal.remove();
|
|
343
|
+
}
|
|
344
|
+
}catch{
|
|
345
|
+
modal.remove();
|
|
346
|
+
}
|
|
347
|
+
finish();
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
modal.addEventListener('modal-closed',finish,{ once:true });
|
|
351
|
+
target.addEventListener?.('pagehide',onPageHide,{ once:true });
|
|
352
|
+
container.append(modal);
|
|
353
|
+
|
|
354
|
+
try{
|
|
355
|
+
await waitForComponent(
|
|
356
|
+
modal,
|
|
357
|
+
{
|
|
358
|
+
event:'modal-ready',
|
|
359
|
+
methods:['populate','open'],
|
|
360
|
+
property:'ready',
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
await modal.populate(content,false);
|
|
364
|
+
await modal.open();
|
|
365
|
+
}catch(error){
|
|
366
|
+
target.removeEventListener?.('pagehide',onPageHide);
|
|
367
|
+
modal.removeEventListener('modal-closed',finish);
|
|
368
|
+
modal.remove();
|
|
369
|
+
resolveClosed();
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
await closed;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
class Errors {
|
|
377
|
+
constructor(options={}) {
|
|
378
|
+
const target=options.target||globalThis.window;
|
|
379
|
+
if(!target||typeof target.addEventListener!=='function'){
|
|
380
|
+
throw new TypeError('Errors requires an event target');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if(options.singleton!==false&&target.errors?.[HANDLER_MARKER]===true){
|
|
384
|
+
return target.errors;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
this[HANDLER_MARKER]=true;
|
|
388
|
+
this.target=target;
|
|
389
|
+
this.delayMs=options.delayMs??DEFAULT_DELAY_MS;
|
|
390
|
+
this.deliveryTimeoutMs=options.deliveryTimeoutMs??45_000;
|
|
391
|
+
this.deliverySchedule=options.deliverySchedule
|
|
392
|
+
|| globalThis.setTimeout.bind(globalThis);
|
|
393
|
+
this.deliveryCancel=options.deliveryCancel
|
|
394
|
+
|| globalThis.clearTimeout.bind(globalThis);
|
|
395
|
+
this.maxPendingIncidents=options.maxPendingIncidents
|
|
396
|
+
?? DEFAULT_MAX_PENDING_INCIDENTS;
|
|
397
|
+
this.maxReportsPerSession=options.maxReportsPerSession
|
|
398
|
+
?? DEFAULT_MAX_REPORTS_PER_SESSION;
|
|
399
|
+
this.maxReportsPerWindow=options.maxReportsPerWindow
|
|
400
|
+
?? DEFAULT_MAX_REPORTS_PER_WINDOW;
|
|
401
|
+
this.rateWindowMs=options.rateWindowMs??DEFAULT_RATE_WINDOW_MS;
|
|
402
|
+
this.now=options.now||Date.now;
|
|
403
|
+
this.schedule=options.schedule||globalThis.setTimeout.bind(globalThis);
|
|
404
|
+
this.cancel=options.cancel||globalThis.clearTimeout.bind(globalThis);
|
|
405
|
+
this.logger=options.logger||globalThis.console;
|
|
406
|
+
this.storage=options.storage===undefined
|
|
407
|
+
? defaultStorage(target)
|
|
408
|
+
: options.storage;
|
|
409
|
+
this.storageHealthy=(
|
|
410
|
+
typeof this.storage?.getItem==='function'
|
|
411
|
+
&& typeof this.storage?.setItem==='function'
|
|
412
|
+
);
|
|
413
|
+
this.sendMail=options.sendMail
|
|
414
|
+
|| sendWithWindowMail.bind(null,target);
|
|
415
|
+
this.isDeveloperMode=typeof options.isDeveloperMode==='function'
|
|
416
|
+
? options.isDeveloperMode
|
|
417
|
+
: defaultDeveloperModeStatus.bind(null,target);
|
|
418
|
+
this.presentDeveloperIncident=typeof options.presentDeveloperIncident==='function'
|
|
419
|
+
? options.presentDeveloperIncident
|
|
420
|
+
: presentDeveloperIncidentModal.bind(null,target);
|
|
421
|
+
|
|
422
|
+
this.pending=new Map();
|
|
423
|
+
this.deliveryQueue=Promise.resolve();
|
|
424
|
+
this.destroyed=false;
|
|
425
|
+
this.invokingMail=false;
|
|
426
|
+
this.deferredDeveloperIncident=null;
|
|
427
|
+
this.developerModalActive=false;
|
|
428
|
+
this.developerUiDisabled=false;
|
|
429
|
+
this.developerShownFingerprints=new Set();
|
|
430
|
+
this.waitingForUser=false;
|
|
431
|
+
|
|
432
|
+
const ledger=this.loadLedger();
|
|
433
|
+
this.reported=new Set(ledger.fingerprints);
|
|
434
|
+
this.reportTimestamps=ledger.reportTimestamps;
|
|
435
|
+
this.circuitOpen=ledger.circuitOpen||!this.storageHealthy;
|
|
436
|
+
|
|
437
|
+
if(!this.storageHealthy){
|
|
438
|
+
this.warn(
|
|
439
|
+
'Persistent suppression storage is unavailable; error email notifications are disabled to prevent reload loops.'
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
this.onError=this.onError.bind(this);
|
|
444
|
+
this.onRejection=this.onRejection.bind(this);
|
|
445
|
+
this.onUserLoaded=this.onUserLoaded.bind(this);
|
|
446
|
+
|
|
447
|
+
target.addEventListener('error',this.onError,true);
|
|
448
|
+
target.addEventListener('unhandledrejection',this.onRejection,true);
|
|
449
|
+
|
|
450
|
+
try{
|
|
451
|
+
target.document?.documentElement?.setAttribute(
|
|
452
|
+
'data-global-error-handler',
|
|
453
|
+
'active'
|
|
454
|
+
);
|
|
455
|
+
}catch{
|
|
456
|
+
// The marker is diagnostic only; listener installation already succeeded.
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if(options.singleton!==false){
|
|
460
|
+
try{
|
|
461
|
+
target.errors=this;
|
|
462
|
+
}catch(error){
|
|
463
|
+
this.warn('Unable to expose the global error-handler singleton.',error);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
this.restorePending(ledger.pending);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
loadLedger(){
|
|
471
|
+
const empty={
|
|
472
|
+
fingerprints:[],
|
|
473
|
+
pending:[],
|
|
474
|
+
reportTimestamps:[],
|
|
475
|
+
circuitOpen:false,
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
if(!this.storageHealthy){
|
|
479
|
+
return empty;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
try{
|
|
483
|
+
const value=JSON.parse(this.storage.getItem(LEDGER_STORAGE_KEY));
|
|
484
|
+
if(!value||typeof value!=='object'){
|
|
485
|
+
return empty;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
fingerprints:Array.isArray(value.fingerprints)
|
|
490
|
+
? value.fingerprints.filter(item => typeof item==='string')
|
|
491
|
+
: [],
|
|
492
|
+
pending:Array.isArray(value.pending)
|
|
493
|
+
? value.pending.filter(record => (
|
|
494
|
+
record
|
|
495
|
+
&& typeof record==='object'
|
|
496
|
+
&& typeof record.fingerprint==='string'
|
|
497
|
+
&& record.incident
|
|
498
|
+
&& typeof record.incident==='object'
|
|
499
|
+
))
|
|
500
|
+
: [],
|
|
501
|
+
reportTimestamps:Array.isArray(value.reportTimestamps)
|
|
502
|
+
? value.reportTimestamps.filter(Number.isFinite)
|
|
503
|
+
: [],
|
|
504
|
+
circuitOpen:value.circuitOpen===true,
|
|
505
|
+
};
|
|
506
|
+
}catch{
|
|
507
|
+
this.storageHealthy=false;
|
|
508
|
+
return empty;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
persistLedger(){
|
|
513
|
+
if(!this.storageHealthy){
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
try{
|
|
518
|
+
const serialized=JSON.stringify({
|
|
519
|
+
fingerprints:[...this.reported],
|
|
520
|
+
pending:[...this.pending.values()].map(record => ({
|
|
521
|
+
count:record.count,
|
|
522
|
+
dueAt:record.dueAt,
|
|
523
|
+
errorStormDetected:record.errorStormDetected,
|
|
524
|
+
fingerprint:record.fingerprint,
|
|
525
|
+
firstSeen:record.firstSeen,
|
|
526
|
+
incident:record.incident,
|
|
527
|
+
lastSeen:record.lastSeen,
|
|
528
|
+
uniqueIncidentCount:record.uniqueIncidentCount,
|
|
529
|
+
})),
|
|
530
|
+
reportTimestamps:this.reportTimestamps,
|
|
531
|
+
circuitOpen:this.circuitOpen,
|
|
532
|
+
});
|
|
533
|
+
this.storage.setItem(LEDGER_STORAGE_KEY,serialized);
|
|
534
|
+
if(this.storage.getItem(LEDGER_STORAGE_KEY)!==serialized){
|
|
535
|
+
throw new Error('Suppression ledger verification failed');
|
|
536
|
+
}
|
|
537
|
+
return true;
|
|
538
|
+
}catch(error){
|
|
539
|
+
this.disableForStorageFailure(error);
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
disableForStorageFailure(error){
|
|
545
|
+
const shouldWarn=this.storageHealthy;
|
|
546
|
+
this.storageHealthy=false;
|
|
547
|
+
this.circuitOpen=true;
|
|
548
|
+
|
|
549
|
+
for(const record of this.pending.values()){
|
|
550
|
+
this.cancelTimer(record);
|
|
551
|
+
}
|
|
552
|
+
this.pending.clear();
|
|
553
|
+
|
|
554
|
+
if(shouldWarn){
|
|
555
|
+
this.warn(
|
|
556
|
+
'Suppression storage failed; error email notifications are disabled to prevent reload loops.',
|
|
557
|
+
error
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
cancelTimer(record){
|
|
563
|
+
if(record?.timer===null||record?.timer===undefined){
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
try{
|
|
568
|
+
this.cancel(record.timer);
|
|
569
|
+
}catch(error){
|
|
570
|
+
this.warn('Unable to cancel an error-notification timer.',error);
|
|
571
|
+
}finally{
|
|
572
|
+
record.timer=null;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
scheduleRecord(record,delayMs){
|
|
577
|
+
try{
|
|
578
|
+
record.timer=this.schedule(
|
|
579
|
+
() => {
|
|
580
|
+
try{
|
|
581
|
+
this.flushFingerprint(record.fingerprint);
|
|
582
|
+
}catch(error){
|
|
583
|
+
this.warn('Unable to flush a scheduled error notification.',error);
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
Math.max(0,delayMs)
|
|
587
|
+
);
|
|
588
|
+
return true;
|
|
589
|
+
}catch(error){
|
|
590
|
+
this.pending.delete(record.fingerprint);
|
|
591
|
+
this.warn('Unable to schedule an error notification.',error);
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
restorePending(records){
|
|
597
|
+
if(this.circuitOpen||!Array.isArray(records)){
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const timestamp=this.now();
|
|
602
|
+
for(const storedRecord of records.slice(0,this.maxPendingIncidents)){
|
|
603
|
+
if(this.reported.has(storedRecord.fingerprint)){
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const firstSeen=Number.isFinite(storedRecord.firstSeen)
|
|
608
|
+
? storedRecord.firstSeen:timestamp;
|
|
609
|
+
const dueAt=Number.isFinite(storedRecord.dueAt)
|
|
610
|
+
? storedRecord.dueAt:firstSeen+this.delayMs;
|
|
611
|
+
const record={
|
|
612
|
+
count:Number.isFinite(storedRecord.count)
|
|
613
|
+
? Math.max(1,storedRecord.count):1,
|
|
614
|
+
dueAt,
|
|
615
|
+
errorStormDetected:storedRecord.errorStormDetected===true,
|
|
616
|
+
fingerprint:storedRecord.fingerprint,
|
|
617
|
+
firstSeen,
|
|
618
|
+
incident:storedRecord.incident,
|
|
619
|
+
lastSeen:Number.isFinite(storedRecord.lastSeen)
|
|
620
|
+
? storedRecord.lastSeen:firstSeen,
|
|
621
|
+
timer:null,
|
|
622
|
+
uniqueIncidentCount:Number.isFinite(storedRecord.uniqueIncidentCount)
|
|
623
|
+
? Math.max(1,storedRecord.uniqueIncidentCount):1,
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
this.pending.set(record.fingerprint,record);
|
|
627
|
+
this.scheduleRecord(record,dueAt-timestamp);
|
|
628
|
+
this.offerDeveloperIncident(record.incident,record.fingerprint);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
this.persistLedger();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
warn(message,error){
|
|
635
|
+
try{
|
|
636
|
+
this.logger?.warn?.(`[global-errors] ${message}`,error);
|
|
637
|
+
}catch{
|
|
638
|
+
// Error reporting must never create another error.
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
onError(event){
|
|
643
|
+
try{
|
|
644
|
+
this.capture(normalizeErrorEvent(event,this.target));
|
|
645
|
+
}catch(error){
|
|
646
|
+
this.warn('Unable to capture a global error.',error);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
onRejection(event){
|
|
651
|
+
try{
|
|
652
|
+
this.capture(normalizeRejectionEvent(event,this.target));
|
|
653
|
+
}catch(error){
|
|
654
|
+
this.warn('Unable to capture an unhandled rejection.',error);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
readDeveloperMode(){
|
|
659
|
+
try{
|
|
660
|
+
return this.isDeveloperMode();
|
|
661
|
+
}catch(error){
|
|
662
|
+
this.warn('Unable to read the developer-mode preference.',error);
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
deferDeveloperIncident(incident,fingerprint){
|
|
668
|
+
if(!this.deferredDeveloperIncident){
|
|
669
|
+
this.deferredDeveloperIncident={ fingerprint,incident };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if(this.waitingForUser){
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
this.waitingForUser=true;
|
|
677
|
+
this.target.addEventListener(
|
|
678
|
+
'user-entity-loaded',
|
|
679
|
+
this.onUserLoaded
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
onUserLoaded(){
|
|
684
|
+
this.target.removeEventListener(
|
|
685
|
+
'user-entity-loaded',
|
|
686
|
+
this.onUserLoaded
|
|
687
|
+
);
|
|
688
|
+
this.waitingForUser=false;
|
|
689
|
+
|
|
690
|
+
const deferred=this.deferredDeveloperIncident;
|
|
691
|
+
this.deferredDeveloperIncident=null;
|
|
692
|
+
|
|
693
|
+
if(deferred){
|
|
694
|
+
this.offerDeveloperIncident(
|
|
695
|
+
deferred.incident,
|
|
696
|
+
deferred.fingerprint,
|
|
697
|
+
false
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
offerDeveloperIncident(incident,fingerprint,allowDefer=true){
|
|
703
|
+
if(
|
|
704
|
+
this.destroyed
|
|
705
|
+
|| this.developerUiDisabled
|
|
706
|
+
|| this.developerModalActive
|
|
707
|
+
|| this.developerShownFingerprints.has(fingerprint)
|
|
708
|
+
){
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const developerMode=this.readDeveloperMode();
|
|
713
|
+
if(developerMode!==true){
|
|
714
|
+
if(allowDefer&&(developerMode===null||developerMode===undefined)){
|
|
715
|
+
this.deferDeveloperIncident(incident,fingerprint);
|
|
716
|
+
}
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
this.developerShownFingerprints.add(fingerprint);
|
|
721
|
+
this.developerModalActive=true;
|
|
722
|
+
|
|
723
|
+
let presentation;
|
|
724
|
+
try{
|
|
725
|
+
presentation=this.presentDeveloperIncident(incident,fingerprint);
|
|
726
|
+
}catch(error){
|
|
727
|
+
this.developerModalActive=false;
|
|
728
|
+
this.developerUiDisabled=true;
|
|
729
|
+
this.warn('Developer error display failed and has been disabled for this session.',error);
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
Promise.resolve(presentation).then(
|
|
734
|
+
() => {
|
|
735
|
+
this.developerModalActive=false;
|
|
736
|
+
},
|
|
737
|
+
error => {
|
|
738
|
+
this.developerModalActive=false;
|
|
739
|
+
this.developerUiDisabled=true;
|
|
740
|
+
this.warn('Developer error display failed and has been disabled for this session.',error);
|
|
741
|
+
}
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
capture(incident){
|
|
748
|
+
if(this.destroyed||this.invokingMail){
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const fingerprint=fingerprintIncident(incident);
|
|
753
|
+
if(this.reported.has(fingerprint)){
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
this.offerDeveloperIncident(incident,fingerprint);
|
|
758
|
+
|
|
759
|
+
if(this.circuitOpen){
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const timestamp=this.now();
|
|
764
|
+
const existing=this.pending.get(fingerprint);
|
|
765
|
+
if(existing){
|
|
766
|
+
existing.count++;
|
|
767
|
+
existing.lastSeen=timestamp;
|
|
768
|
+
if(existing.count===2){
|
|
769
|
+
return this.persistLedger();
|
|
770
|
+
}
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if(this.pending.size>=this.maxPendingIncidents){
|
|
775
|
+
const stormRecord=this.pending.values().next().value;
|
|
776
|
+
if(stormRecord){
|
|
777
|
+
const stormAlreadyDetected=stormRecord.errorStormDetected;
|
|
778
|
+
stormRecord.errorStormDetected=true;
|
|
779
|
+
stormRecord.lastSeen=timestamp;
|
|
780
|
+
stormRecord.uniqueIncidentCount=stormAlreadyDetected
|
|
781
|
+
? stormRecord.uniqueIncidentCount+1
|
|
782
|
+
: this.pending.size+1;
|
|
783
|
+
this.persistLedger();
|
|
784
|
+
}
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const record={
|
|
789
|
+
count:1,
|
|
790
|
+
dueAt:timestamp+this.delayMs,
|
|
791
|
+
errorStormDetected:false,
|
|
792
|
+
fingerprint,
|
|
793
|
+
firstSeen:timestamp,
|
|
794
|
+
incident,
|
|
795
|
+
lastSeen:timestamp,
|
|
796
|
+
timer:null,
|
|
797
|
+
uniqueIncidentCount:1,
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
this.pending.set(fingerprint,record);
|
|
801
|
+
// This is intentionally a fixed window from the first occurrence.
|
|
802
|
+
// Resetting the timer on every repeat could postpone a true loop forever.
|
|
803
|
+
if(!this.scheduleRecord(record,this.delayMs)){
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return this.persistLedger();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
reserveDelivery(timestamp){
|
|
811
|
+
const cutoff=timestamp-this.rateWindowMs;
|
|
812
|
+
const recentReportTimestamps=this.reportTimestamps.filter(
|
|
813
|
+
reportTimestamp => reportTimestamp>=cutoff
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
if(
|
|
817
|
+
this.reportTimestamps.length>=this.maxReportsPerSession
|
|
818
|
+
|| recentReportTimestamps.length>=this.maxReportsPerWindow
|
|
819
|
+
){
|
|
820
|
+
this.openCircuit();
|
|
821
|
+
return { allowed:false,circuitOpened:true };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
this.reportTimestamps.push(timestamp);
|
|
825
|
+
|
|
826
|
+
const circuitOpened=(
|
|
827
|
+
this.reportTimestamps.length>=this.maxReportsPerSession
|
|
828
|
+
|| recentReportTimestamps.length+1>=this.maxReportsPerWindow
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
if(circuitOpened){
|
|
832
|
+
this.openCircuit();
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return { allowed:true,circuitOpened };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
openCircuit(){
|
|
839
|
+
this.circuitOpen=true;
|
|
840
|
+
|
|
841
|
+
for(const record of this.pending.values()){
|
|
842
|
+
this.cancelTimer(record);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
this.pending.clear();
|
|
846
|
+
this.persistLedger();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
flushFingerprint(fingerprint){
|
|
850
|
+
const record=this.pending.get(fingerprint);
|
|
851
|
+
if(!record){
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
this.pending.delete(fingerprint);
|
|
856
|
+
this.cancelTimer(record);
|
|
857
|
+
|
|
858
|
+
if(this.destroyed||this.circuitOpen||this.reported.has(fingerprint)){
|
|
859
|
+
this.persistLedger();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Suppress before performing any fallible notification work. A failed
|
|
864
|
+
// mail request is an attempted report and must never be retried recursively.
|
|
865
|
+
this.reported.add(fingerprint);
|
|
866
|
+
const reservation=this.reserveDelivery(this.now());
|
|
867
|
+
if(record.errorStormDetected&&reservation.allowed&&!reservation.circuitOpened){
|
|
868
|
+
reservation.circuitOpened=true;
|
|
869
|
+
this.openCircuit();
|
|
870
|
+
}
|
|
871
|
+
const persisted=this.persistLedger();
|
|
872
|
+
|
|
873
|
+
if(!reservation.allowed||!persisted){
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
this.deliveryQueue=this.deliveryQueue
|
|
878
|
+
.then(() => this.deliver(record,reservation))
|
|
879
|
+
.catch(error => {
|
|
880
|
+
this.warn('Unexpected notification-queue failure; no retry will be attempted.',error);
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
buildNotification(record,reservation){
|
|
885
|
+
const loopDetected=record.count>1;
|
|
886
|
+
const errorStormDetected=record.errorStormDetected
|
|
887
|
+
|| reservation.circuitOpened;
|
|
888
|
+
const baseSubject=record.incident.type==='unhandledrejection'
|
|
889
|
+
? 'ARCANE JS UNHANDLED REJECTION'
|
|
890
|
+
: 'ARCANE JS ERROR';
|
|
891
|
+
|
|
892
|
+
let subject=baseSubject;
|
|
893
|
+
if(errorStormDetected){
|
|
894
|
+
subject=`${baseSubject} - ERROR STORM DETECTED`;
|
|
895
|
+
}else if(loopDetected){
|
|
896
|
+
subject=`${baseSubject} - LOOP DETECTED`;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const payload={
|
|
900
|
+
...record.incident,
|
|
901
|
+
fingerprint:record.fingerprint,
|
|
902
|
+
occurrence_count:record.count,
|
|
903
|
+
first_seen_at:safeIso(record.firstSeen),
|
|
904
|
+
last_seen_at:safeIso(record.lastSeen),
|
|
905
|
+
observation_window_ms:this.delayMs,
|
|
906
|
+
loop_detected:loopDetected,
|
|
907
|
+
error_storm_detected:errorStormDetected,
|
|
908
|
+
unique_incident_count:record.uniqueIncidentCount,
|
|
909
|
+
matching_notifications_suppressed:true,
|
|
910
|
+
loop_notice:loopDetected
|
|
911
|
+
? `This error repeated ${record.count} times during the ${this.delayMs}ms observation window. Further matching notifications are suppressed for this browser session.`
|
|
912
|
+
: 'Further occurrences of this same error are suppressed for this browser session.',
|
|
913
|
+
circuit_breaker_notice:errorStormDetected
|
|
914
|
+
? 'The global notification limit was reached. Further error emails are suppressed for this browser session to stop a possible varying error loop.'
|
|
915
|
+
: null,
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
return { payload,subject };
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async deliver(record,reservation){
|
|
922
|
+
const { payload,subject }=this.buildNotification(record,reservation);
|
|
923
|
+
let timeout=null;
|
|
924
|
+
const delivery=Promise.resolve().then(() => {
|
|
925
|
+
// Only suppress events emitted synchronously by the adapter call.
|
|
926
|
+
// Its returned promise is observed below, so unrelated errors that
|
|
927
|
+
// occur while network delivery is pending must still be captured.
|
|
928
|
+
this.invokingMail=true;
|
|
929
|
+
try{
|
|
930
|
+
return this.sendMail([],subject,payload,MESSAGE_STYLE,'error');
|
|
931
|
+
}finally{
|
|
932
|
+
this.invokingMail=false;
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
try{
|
|
937
|
+
await Promise.race([
|
|
938
|
+
delivery,
|
|
939
|
+
new Promise((resolve,reject) => {
|
|
940
|
+
timeout=this.deliverySchedule(
|
|
941
|
+
() => reject(new Error('Error notification timed out')),
|
|
942
|
+
this.deliveryTimeoutMs
|
|
943
|
+
);
|
|
944
|
+
}),
|
|
945
|
+
]);
|
|
946
|
+
}catch(error){
|
|
947
|
+
this.warn('Error notification failed; no retry will be attempted.',error);
|
|
948
|
+
}finally{
|
|
949
|
+
if(timeout!==null){
|
|
950
|
+
try{
|
|
951
|
+
this.deliveryCancel(timeout);
|
|
952
|
+
}catch(error){
|
|
953
|
+
this.warn('Unable to cancel the notification timeout.',error);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
async flush(){
|
|
960
|
+
for(const fingerprint of [...this.pending.keys()]){
|
|
961
|
+
this.flushFingerprint(fingerprint);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
await this.whenIdle();
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
async whenIdle(){
|
|
968
|
+
await this.deliveryQueue;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
destroy(){
|
|
972
|
+
if(this.destroyed){
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
this.destroyed=true;
|
|
977
|
+
this.target.removeEventListener('error',this.onError,true);
|
|
978
|
+
this.target.removeEventListener('unhandledrejection',this.onRejection,true);
|
|
979
|
+
this.target.removeEventListener('user-entity-loaded',this.onUserLoaded);
|
|
980
|
+
this.waitingForUser=false;
|
|
981
|
+
this.deferredDeveloperIncident=null;
|
|
982
|
+
|
|
983
|
+
try{
|
|
984
|
+
const modal=this.target.document?.querySelector?.(
|
|
985
|
+
'html-import[data-global-error-modal]'
|
|
986
|
+
);
|
|
987
|
+
if(typeof modal?.close==='function'){
|
|
988
|
+
modal.close(undefined,true);
|
|
989
|
+
}else{
|
|
990
|
+
modal?.remove?.();
|
|
991
|
+
}
|
|
992
|
+
}catch(error){
|
|
993
|
+
this.warn('Unable to remove the developer error modal.',error);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
try{
|
|
997
|
+
this.target.document?.documentElement?.removeAttribute(
|
|
998
|
+
'data-global-error-handler'
|
|
999
|
+
);
|
|
1000
|
+
}catch{
|
|
1001
|
+
// The marker is diagnostic only.
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
for(const record of this.pending.values()){
|
|
1005
|
+
this.cancelTimer(record);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
this.pending.clear();
|
|
1009
|
+
this.persistLedger();
|
|
1010
|
+
|
|
1011
|
+
if(this.target.errors===this){
|
|
1012
|
+
try{
|
|
1013
|
+
delete this.target.errors;
|
|
1014
|
+
}catch{
|
|
1015
|
+
this.target.errors=undefined;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if(typeof window!=='undefined'&&window.errors?.[HANDLER_MARKER]!==true){
|
|
1022
|
+
new Errors();
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export default Errors;
|