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.
- package/CHANGELOG.md +29 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
- package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
- package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
- package/browser-runtime/ai/browser-speech.mjs +2 -0
- package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
- package/browser-runtime/ai/model-controller.mjs +285 -95
- package/browser-runtime/ai/speech-worker-client.mjs +247 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
- package/browser-runtime/event-manager.mjs +1097 -1
- package/docs/architecture.md +2 -2
- package/docs/event-manager.md +155 -27
- package/docs/reference/README.md +27 -27
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1162 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +6 -3
- package/docs/reference/behavioral-testing.md +29 -6
- package/docs/reference/cli.md +117 -9
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/event-manager.md +577 -32
- package/docs/reference/inventory/package-api.json +478 -2
- package/docs/reference/inventory/runtime-components.json +108 -44
- package/docs/reference/inventory/runtime-modules.json +131 -53
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +157 -43
- package/docs/reference/runtime-components.md +258 -83
- package/docs/reference/runtime-modules.md +613 -77
- package/docs/reference/sdk-api.md +1014 -25
- package/package.json +5 -4
- package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
- package/runtime/arcane/components/app-bar.html +34 -13
- package/runtime/arcane/components/assistant-panel.html +110 -57
- package/runtime/arcane/components/calculator.html +7 -4
- package/runtime/arcane/components/chart.html +58 -17
- package/runtime/arcane/components/chat.html +606 -136
- package/runtime/arcane/components/conversation-view.html +13 -6
- package/runtime/arcane/components/dashboard-config.html +96 -59
- package/runtime/arcane/components/data-maintenance.html +69 -14
- package/runtime/arcane/components/data-view.html +53 -7
- package/runtime/arcane/components/directory-picker.html +118 -32
- package/runtime/arcane/components/document-inspector.html +47 -10
- package/runtime/arcane/components/file-drop.html +81 -35
- package/runtime/arcane/components/file-inspector.html +72 -22
- package/runtime/arcane/components/file-manager.html +374 -79
- package/runtime/arcane/components/integration-settings.html +12 -5
- package/runtime/arcane/components/local-ai-status.html +48 -19
- package/runtime/arcane/components/markdown-document.html +161 -68
- package/runtime/arcane/components/markdown-editor.html +110 -33
- package/runtime/arcane/components/media-embed.html +8 -5
- package/runtime/arcane/components/modal.html +15 -5
- package/runtime/arcane/components/output-panel.html +28 -23
- package/runtime/arcane/components/preferences-form.html +22 -4
- package/runtime/arcane/components/record-timeline.html +18 -2
- package/runtime/arcane/components/relationship-board.html +23 -3
- package/runtime/arcane/components/screen-capture.html +10 -4
- package/runtime/arcane/components/source-code-viewer.html +76 -9
- package/runtime/arcane/components/source-explanation.html +23 -3
- package/runtime/arcane/components/speech.html +462 -384
- package/runtime/arcane/components/summary-strip.html +22 -11
- package/runtime/arcane/components/table.html +39 -21
- package/runtime/arcane/components/task-progress.html +79 -21
- package/runtime/arcane/components/terminal-workspace.html +7 -4
- package/runtime/arcane/components/theme-editor.html +7 -3
- package/runtime/arcane/components/unified-inbox.html +9 -4
- package/runtime/arcane/components/voice-transcription.html +639 -98
- package/runtime/arcane/components/weather-widget.html +5 -3
- package/runtime/arcane/components/web-navigator.html +48 -8
- package/runtime/arcane/entities/Chat.js +1 -1
- package/runtime/arcane/entities/User.js +110 -23
- package/runtime/arcane/modules/AI.js +2109 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
- package/runtime/arcane/modules/AIRuntimeState.js +109 -52
- package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
- package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
- package/runtime/arcane/modules/CalculatorEngine.js +63 -3
- package/runtime/arcane/modules/CommunicationAppController.js +588 -28
- package/runtime/arcane/modules/CommunicationHub.js +590 -10
- package/runtime/arcane/modules/ComponentContracts.js +470 -0
- package/runtime/arcane/modules/ConversationTimebox.js +152 -33
- package/runtime/arcane/modules/DBLS.js +40 -7
- package/runtime/arcane/modules/DBOPFS.js +35 -11
- package/runtime/arcane/modules/DataMaintenance.js +12 -2
- package/runtime/arcane/modules/Errors.js +65 -7
- package/runtime/arcane/modules/HTMLImport.js +198 -14
- package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
- package/runtime/arcane/modules/Mail.js +738 -115
- package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
- package/runtime/arcane/modules/MailTransport.mjs +197 -39
- package/runtime/arcane/modules/Ollama.js +36 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
- package/runtime/arcane/modules/PreferenceStore.js +367 -33
- package/runtime/arcane/modules/RecordReviewStore.js +322 -23
- package/runtime/arcane/modules/ScreenCapture.js +1397 -15
- package/runtime/arcane/modules/SpeechPlayback.js +438 -41
- package/runtime/arcane/modules/TerminalClient.js +277 -12
- package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
- package/runtime/arcane/modules/ThemeManager.js +39 -7
- package/runtime/arcane/modules/TimeGuard.js +131 -20
- package/runtime/arcane/modules/WaitForComponent.js +386 -33
- package/schemas/arcane-lock.schema.json +2 -2
- package/src/cli/main.mjs +435 -9
- package/src/event-manager.mjs +1097 -1
- package/src/import-map.mjs +21 -3
- package/src/index.mjs +13 -0
- package/src/installed-sdk-runtime.mjs +112 -0
- package/src/mail-api.mjs +22 -0
- package/src/mail-credentials.mjs +667 -0
- package/src/mail-server.mjs +1769 -0
- package/src/mail.mjs +261 -0
- package/src/sdk-browser-runtime.mjs +85 -41
- package/src/testing-loader.mjs +7 -0
- package/src/toolchain.mjs +3 -0
- package/src/workspace.mjs +1 -1
|
@@ -1,14 +1,590 @@
|
|
|
1
|
+
import {createArcaneEventSource} from 'arcane-os/event-manager';
|
|
1
2
|
import ApiModelDatabase from './ApiModelDatabase.js';
|
|
2
3
|
import {WeatherDay,WeatherLocation,WeatherObservation,WeatherSnapshot} from '../entities/Weather.js';
|
|
3
4
|
|
|
4
|
-
export const OPEN_METEO_ENDPOINTS=Object.freeze({
|
|
5
|
+
export const OPEN_METEO_ENDPOINTS=Object.freeze({
|
|
6
|
+
geocoding:'https://geocoding-api.open-meteo.com/v1/search',
|
|
7
|
+
forecast:'https://api.open-meteo.com/v1/forecast'
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const OPEN_METEO_WEATHER_EVENTS=Object.freeze({
|
|
11
|
+
requestStarted:'weather-request',
|
|
12
|
+
requestFailed:'weather-error',
|
|
13
|
+
locationSearchSucceeded:'weather-locations',
|
|
14
|
+
forecastLoadSucceeded:'weather-weather'
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const WEATHER_EVENT_TYPES=Object.freeze(Object.values(OPEN_METEO_WEATHER_EVENTS));
|
|
18
|
+
|
|
19
|
+
export const OPEN_METEO_WEATHER_ERRORS=Object.freeze({
|
|
20
|
+
providerDisposed:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_PROVIDER_DISPOSED',reason:'open-meteo-weather-provider-disposed'}),
|
|
21
|
+
weatherEventTypeInvalid:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_EVENT_TYPE_INVALID',reason:'weather-event-type-invalid'}),
|
|
22
|
+
weatherLocationInvalid:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_INVALID',reason:'weather-location-invalid'}),
|
|
23
|
+
weatherOperationOptionsInvalid:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_OPERATION_OPTIONS_INVALID',reason:'weather-operation-options-invalid'}),
|
|
24
|
+
weatherLocationQueryInvalid:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_QUERY_INVALID',reason:'weather-location-query-invalid'}),
|
|
25
|
+
locationSearchAborted:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_SEARCH_ABORTED',reason:'weather-location-search-aborted'}),
|
|
26
|
+
locationSearchFailed:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_SEARCH_FAILED',reason:'weather-location-search-rejected'}),
|
|
27
|
+
locationSearchSuperseded:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_SEARCH_SUPERSEDED',reason:'weather-location-search-superseded'}),
|
|
28
|
+
forecastLoadAborted:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_FORECAST_LOAD_ABORTED',reason:'weather-forecast-load-aborted'}),
|
|
29
|
+
forecastLoadFailed:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_FORECAST_LOAD_FAILED',reason:'weather-forecast-load-rejected'}),
|
|
30
|
+
forecastLoadSuperseded:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_FORECAST_LOAD_SUPERSEDED',reason:'weather-forecast-load-superseded'})
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const WEATHER_EVENT_NAMES=Object.freeze({
|
|
34
|
+
error:OPEN_METEO_WEATHER_EVENTS.requestFailed,
|
|
35
|
+
locations:OPEN_METEO_WEATHER_EVENTS.locationSearchSucceeded,
|
|
36
|
+
request:OPEN_METEO_WEATHER_EVENTS.requestStarted,
|
|
37
|
+
weather:OPEN_METEO_WEATHER_EVENTS.forecastLoadSucceeded
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function signalLike(value){
|
|
41
|
+
return value===undefined
|
|
42
|
+
||value===null
|
|
43
|
+
||(
|
|
44
|
+
typeof value==='object'
|
|
45
|
+
&&typeof value.aborted==='boolean'
|
|
46
|
+
&&typeof value.addEventListener==='function'
|
|
47
|
+
&&typeof value.removeEventListener==='function'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function defineWeatherError(error,contract,message){
|
|
52
|
+
const candidate=error&&(typeof error==='object'||typeof error==='function')
|
|
53
|
+
?error
|
|
54
|
+
:new Error(typeof error==='string'&&error.trim()?error:message);
|
|
55
|
+
const priorCode=typeof candidate.code==='string'&&candidate.code?candidate.code:null;
|
|
56
|
+
try{
|
|
57
|
+
if(priorCode&&priorCode!==contract.code&&!Object.hasOwn(candidate,'providerCode')){
|
|
58
|
+
Object.defineProperty(candidate,'providerCode',{configurable:true,enumerable:false,value:priorCode,writable:true});
|
|
59
|
+
}
|
|
60
|
+
Object.defineProperty(candidate,'code',{configurable:true,enumerable:false,value:contract.code,writable:true});
|
|
61
|
+
Object.defineProperty(candidate,'reason',{configurable:true,enumerable:false,value:contract.reason,writable:true});
|
|
62
|
+
if(error!==candidate&&!('cause' in candidate)){
|
|
63
|
+
Object.defineProperty(candidate,'cause',{configurable:true,enumerable:false,value:error,writable:true});
|
|
64
|
+
}
|
|
65
|
+
return candidate;
|
|
66
|
+
}catch{
|
|
67
|
+
const replacement=new Error(
|
|
68
|
+
typeof candidate.message==='string'&&candidate.message.trim()?candidate.message:message
|
|
69
|
+
);
|
|
70
|
+
replacement.code=contract.code;
|
|
71
|
+
replacement.reason=contract.reason;
|
|
72
|
+
if(priorCode&&priorCode!==contract.code)replacement.providerCode=priorCode;
|
|
73
|
+
replacement.cause=candidate;
|
|
74
|
+
return replacement;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function invalidOptionsError(message){
|
|
79
|
+
return defineWeatherError(new TypeError(message),OPEN_METEO_WEATHER_ERRORS.weatherOperationOptionsInvalid,message);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function operationContract(kind,outcome){
|
|
83
|
+
if(kind==='location-search'){
|
|
84
|
+
if(outcome==='aborted')return OPEN_METEO_WEATHER_ERRORS.locationSearchAborted;
|
|
85
|
+
if(outcome==='superseded')return OPEN_METEO_WEATHER_ERRORS.locationSearchSuperseded;
|
|
86
|
+
return OPEN_METEO_WEATHER_ERRORS.locationSearchFailed;
|
|
87
|
+
}
|
|
88
|
+
if(outcome==='aborted')return OPEN_METEO_WEATHER_ERRORS.forecastLoadAborted;
|
|
89
|
+
if(outcome==='superseded')return OPEN_METEO_WEATHER_ERRORS.forecastLoadSuperseded;
|
|
90
|
+
return OPEN_METEO_WEATHER_ERRORS.forecastLoadFailed;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function operationMessage(kind,outcome){
|
|
94
|
+
const subject=kind==='location-search'?'weather location search':'weather forecast load';
|
|
95
|
+
if(outcome==='aborted')return `The ${subject} was aborted.`;
|
|
96
|
+
if(outcome==='superseded')return `The ${subject} was superseded by a newer request.`;
|
|
97
|
+
return `The ${subject} failed.`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizedOperationError(error,record){
|
|
101
|
+
if(record.terminalError)return record.terminalError;
|
|
102
|
+
const aborted=record.controller.signal.aborted
|
|
103
|
+
||error?.code==='ARCANE_API_MODEL_REQUEST_ABORTED';
|
|
104
|
+
const outcome=aborted?'aborted':'failed';
|
|
105
|
+
return defineWeatherError(
|
|
106
|
+
record.controller.signal.aborted?(record.controller.signal.reason??error):error,
|
|
107
|
+
operationContract(record.kind,outcome),
|
|
108
|
+
operationMessage(record.kind,outcome)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function operationOptions(value){
|
|
113
|
+
if(value===undefined)return Object.freeze({signal:null});
|
|
114
|
+
if(!value||typeof value!=='object'||Array.isArray(value)){
|
|
115
|
+
throw invalidOptionsError('Open-Meteo operation options must be an object.');
|
|
116
|
+
}
|
|
117
|
+
if(!signalLike(value.signal)){
|
|
118
|
+
throw invalidOptionsError('Open-Meteo operation signal must be an AbortSignal.');
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function linkAbortSignal(signal,controller,cleanup){
|
|
124
|
+
if(signal===null||signal===undefined)return;
|
|
125
|
+
function abortWeatherOperation(){
|
|
126
|
+
if(!controller.signal.aborted)controller.abort(signal.reason);
|
|
127
|
+
}
|
|
128
|
+
if(signal.aborted){
|
|
129
|
+
abortWeatherOperation();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
signal.addEventListener('abort',abortWeatherOperation,{once:true});
|
|
133
|
+
cleanup.push(function removeWeatherAbortListener(){
|
|
134
|
+
signal.removeEventListener('abort',abortWeatherOperation);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function parseLocations(raw){
|
|
139
|
+
return Array.from(raw.results||[],function createWeatherLocation(item){
|
|
140
|
+
return new WeatherLocation({
|
|
141
|
+
id:item.id,
|
|
142
|
+
name:item.name,
|
|
143
|
+
region:item.admin1||'',
|
|
144
|
+
country:item.country||'',
|
|
145
|
+
latitude:item.latitude,
|
|
146
|
+
longitude:item.longitude,
|
|
147
|
+
timezone:item.timezone||'auto'
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseForecast(raw,{context}){return mapForecast(raw,context.location);}
|
|
5
153
|
|
|
6
154
|
export default class OpenMeteoWeatherProvider extends EventTarget{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
155
|
+
#activeLoad=null;
|
|
156
|
+
#activeSearch=null;
|
|
157
|
+
#disposed=false;
|
|
158
|
+
#events;
|
|
159
|
+
#loadGeneration=0;
|
|
160
|
+
#operationSequence=0;
|
|
161
|
+
#operations=new Map();
|
|
162
|
+
#searchGeneration=0;
|
|
163
|
+
#unsubscribe=[];
|
|
164
|
+
|
|
165
|
+
constructor({
|
|
166
|
+
geocodingEndpoint=OPEN_METEO_ENDPOINTS.geocoding,
|
|
167
|
+
forecastEndpoint=OPEN_METEO_ENDPOINTS.forecast,
|
|
168
|
+
fetchImpl=globalThis.fetch
|
|
169
|
+
}={}){
|
|
170
|
+
super();
|
|
171
|
+
this.geocoder=new ApiModelDatabase({
|
|
172
|
+
endpoint:geocodingEndpoint,
|
|
173
|
+
fetchImpl,
|
|
174
|
+
parser:parseLocations
|
|
175
|
+
});
|
|
176
|
+
this.forecast=new ApiModelDatabase({
|
|
177
|
+
endpoint:forecastEndpoint,
|
|
178
|
+
fetchImpl,
|
|
179
|
+
parser:parseForecast
|
|
180
|
+
});
|
|
181
|
+
this.#events=createArcaneEventSource(this,{
|
|
182
|
+
source:'open-meteo-weather-provider',
|
|
183
|
+
eventTypes:WEATHER_EVENT_TYPES
|
|
184
|
+
});
|
|
185
|
+
const provider=this;
|
|
186
|
+
function forwardGeocoderRequest(event){provider.#forwardRequest('location-search',event);}
|
|
187
|
+
function forwardGeocoderError(event){provider.#forwardError('location-search',event);}
|
|
188
|
+
function forwardForecastRequest(event){provider.#forwardRequest('forecast-load',event);}
|
|
189
|
+
function forwardForecastError(event){provider.#forwardError('forecast-load',event);}
|
|
190
|
+
this.#unsubscribe.push(
|
|
191
|
+
this.geocoder.on('api-model-request',forwardGeocoderRequest),
|
|
192
|
+
this.geocoder.on('api-model-error',forwardGeocoderError),
|
|
193
|
+
this.forecast.on('api-model-request',forwardForecastRequest),
|
|
194
|
+
this.forecast.on('api-model-error',forwardForecastError)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
addEventListener(type,listener,options){return this.#events.addEventListener(type,listener,options);}
|
|
199
|
+
removeEventListener(type,listener,options){return this.#events.removeEventListener(type,listener,options);}
|
|
200
|
+
on(type,listener,options){return this.#events.on(type,listener,options);}
|
|
201
|
+
dispatchEvent(value){return this.#events.dispatchEvent(value);}
|
|
202
|
+
|
|
203
|
+
#assertOpen(){
|
|
204
|
+
if(this.#disposed){
|
|
205
|
+
throw defineWeatherError(
|
|
206
|
+
new Error('The Open-Meteo weather provider has been disposed.'),
|
|
207
|
+
OPEN_METEO_WEATHER_ERRORS.providerDisposed,
|
|
208
|
+
'The Open-Meteo weather provider has been disposed.'
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#currentOperation(record){
|
|
214
|
+
return record.kind==='location-search'
|
|
215
|
+
?this.#activeSearch===record&&this.#searchGeneration===record.generation
|
|
216
|
+
:this.#activeLoad===record&&this.#loadGeneration===record.generation;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#releaseSignal(record){
|
|
220
|
+
if(!Array.isArray(record.cleanup))return;
|
|
221
|
+
for(const remove of record.cleanup.splice(0))remove();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#settleOperation(record){
|
|
225
|
+
if(record.settled)return;
|
|
226
|
+
record.settled=true;
|
|
227
|
+
this.#releaseSignal(record);
|
|
228
|
+
if(record.kind==='location-search'&&this.#activeSearch===record)this.#activeSearch=null;
|
|
229
|
+
if(record.kind==='forecast-load'&&this.#activeLoad===record)this.#activeLoad=null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#finishOperation(record){
|
|
233
|
+
this.#settleOperation(record);
|
|
234
|
+
this.#operations.delete(record.operationId);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#dispatch(type,detail,operationId){
|
|
238
|
+
const eventType=WEATHER_EVENT_NAMES[type];
|
|
239
|
+
if(!eventType){
|
|
240
|
+
throw defineWeatherError(
|
|
241
|
+
new TypeError(`Unknown weather event type: ${String(type)}.`),
|
|
242
|
+
OPEN_METEO_WEATHER_ERRORS.weatherEventTypeInvalid,
|
|
243
|
+
'The weather event type is invalid.'
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
const compatibilityDetail=Object.freeze({...detail,operationId});
|
|
247
|
+
const operation=typeof detail?.operation==='string'?detail.operation:null;
|
|
248
|
+
let publicDetail;
|
|
249
|
+
if(type==='request'){
|
|
250
|
+
publicDetail=Object.freeze(operation?{operation}:{});
|
|
251
|
+
}else if(type==='error'){
|
|
252
|
+
publicDetail=Object.freeze({
|
|
253
|
+
...(operation?{operation}:{}),
|
|
254
|
+
...(typeof detail?.error?.code==='string'?{code:detail.error.code}:{}),
|
|
255
|
+
...(typeof detail?.error?.reason==='string'?{reason:detail.error.reason}:{})
|
|
256
|
+
});
|
|
257
|
+
}else if(type==='locations'){
|
|
258
|
+
publicDetail=Object.freeze({
|
|
259
|
+
...(operation?{operation}:{}),
|
|
260
|
+
count:Array.isArray(detail?.locations)?detail.locations.length:0
|
|
261
|
+
});
|
|
262
|
+
}else{
|
|
263
|
+
publicDetail=Object.freeze({
|
|
264
|
+
...(operation?{operation}:{}),
|
|
265
|
+
...(typeof detail?.weather?.location?.id==='string'
|
|
266
|
+
?{locationId:detail.weather.location.id}
|
|
267
|
+
:{})
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return this.#events.dispatch(eventType,compatibilityDetail,{operationId,publicDetail});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#publishError(record,error,childDetail={}){
|
|
274
|
+
if(record.errorPublished||this.#events.disposed)return false;
|
|
275
|
+
record.errorPublished=true;
|
|
276
|
+
record.publicError=error;
|
|
277
|
+
record.terminalError??=error;
|
|
278
|
+
this.#settleOperation(record);
|
|
279
|
+
this.#dispatch(
|
|
280
|
+
'error',
|
|
281
|
+
{
|
|
282
|
+
...childDetail,
|
|
283
|
+
requestId:record.operationId,
|
|
284
|
+
operation:record.kind,
|
|
285
|
+
error,
|
|
286
|
+
reason:error.reason
|
|
287
|
+
},
|
|
288
|
+
record.operationId
|
|
289
|
+
);
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#terminateOperation(record,error){
|
|
294
|
+
if(record.terminalError)return record.terminalError;
|
|
295
|
+
record.terminalError=error;
|
|
296
|
+
this.#publishError(record,error);
|
|
297
|
+
if(!record.controller.signal.aborted)record.controller.abort(error);
|
|
298
|
+
this.#releaseSignal(record);
|
|
299
|
+
return error;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#startOperation(kind,signal){
|
|
303
|
+
this.#assertOpen();
|
|
304
|
+
if(!signalLike(signal))throw invalidOptionsError('Open-Meteo operation signal must be an AbortSignal.');
|
|
305
|
+
if(signal?.aborted){
|
|
306
|
+
throw defineWeatherError(
|
|
307
|
+
signal.reason,
|
|
308
|
+
operationContract(kind,'aborted'),
|
|
309
|
+
operationMessage(kind,'aborted')
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
const generation=kind==='location-search'
|
|
313
|
+
?++this.#searchGeneration
|
|
314
|
+
:++this.#loadGeneration;
|
|
315
|
+
const operationId=`${this.#events.instanceId}:${kind}:${(++this.#operationSequence).toString(36)}`;
|
|
316
|
+
const controller=new AbortController();
|
|
317
|
+
const record={
|
|
318
|
+
cleanup:[],
|
|
319
|
+
controller,
|
|
320
|
+
errorPublished:false,
|
|
321
|
+
generation,
|
|
322
|
+
kind,
|
|
323
|
+
operationId,
|
|
324
|
+
publicError:null,
|
|
325
|
+
settled:false,
|
|
326
|
+
terminalError:null
|
|
327
|
+
};
|
|
328
|
+
linkAbortSignal(signal??null,controller,record.cleanup);
|
|
329
|
+
const previous=kind==='location-search'?this.#activeSearch:this.#activeLoad;
|
|
330
|
+
if(kind==='location-search')this.#activeSearch=record;
|
|
331
|
+
else this.#activeLoad=record;
|
|
332
|
+
this.#operations.set(operationId,record);
|
|
333
|
+
if(previous){
|
|
334
|
+
this.#terminateOperation(
|
|
335
|
+
previous,
|
|
336
|
+
defineWeatherError(
|
|
337
|
+
new Error(operationMessage(kind,'superseded')),
|
|
338
|
+
operationContract(kind,'superseded'),
|
|
339
|
+
operationMessage(kind,'superseded')
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return record;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#forwardRequest(kind,event){
|
|
347
|
+
if(this.#disposed||this.#events.disposed)return;
|
|
348
|
+
const operationId=event?.operationId??event?.detail?.requestId;
|
|
349
|
+
if(typeof operationId!=='string'||!operationId)return;
|
|
350
|
+
const record=this.#operations.get(operationId);
|
|
351
|
+
if(record&&!this.#currentOperation(record))return;
|
|
352
|
+
this.#dispatch(
|
|
353
|
+
'request',
|
|
354
|
+
{...(event.detail||{}),operation:record?.kind??kind},
|
|
355
|
+
operationId
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#forwardError(kind,event){
|
|
360
|
+
if(this.#disposed||this.#events.disposed)return;
|
|
361
|
+
const operationId=event?.operationId??event?.detail?.requestId;
|
|
362
|
+
if(typeof operationId!=='string'||!operationId)return;
|
|
363
|
+
const record=this.#operations.get(operationId);
|
|
364
|
+
if(record){
|
|
365
|
+
if(record.errorPublished)return;
|
|
366
|
+
this.#publishError(record,normalizedOperationError(event.detail?.error,record),event.detail);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const directRecord={
|
|
370
|
+
cleanup:[],
|
|
371
|
+
controller:{signal:{aborted:false}},
|
|
372
|
+
errorPublished:false,
|
|
373
|
+
kind,
|
|
374
|
+
operationId,
|
|
375
|
+
publicError:null,
|
|
376
|
+
settled:false,
|
|
377
|
+
terminalError:null
|
|
378
|
+
};
|
|
379
|
+
this.#publishError(directRecord,normalizedOperationError(event.detail?.error,directRecord),event.detail);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
setEndpoints({geocoding,forecast}={}){
|
|
383
|
+
this.#assertOpen();
|
|
384
|
+
if(geocoding)this.geocoder.setEndpoint(geocoding);
|
|
385
|
+
if(forecast)this.forecast.setEndpoint(forecast);
|
|
386
|
+
return Object.freeze({
|
|
387
|
+
geocoding:this.geocoder.endpoint,
|
|
388
|
+
forecast:this.forecast.endpoint
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async search(query,optionsValue={}){
|
|
393
|
+
this.#assertOpen();
|
|
394
|
+
const name=String(query||'').trim();
|
|
395
|
+
if(name.length<2){
|
|
396
|
+
throw defineWeatherError(
|
|
397
|
+
new TypeError('Enter at least two characters for a location search.'),
|
|
398
|
+
OPEN_METEO_WEATHER_ERRORS.weatherLocationQueryInvalid,
|
|
399
|
+
'Enter at least two characters for a location search.'
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
const options=operationOptions(optionsValue);
|
|
403
|
+
const record=this.#startOperation('location-search',options.signal??null);
|
|
404
|
+
try{
|
|
405
|
+
const result=await this.geocoder.fetch(
|
|
406
|
+
{name,count:8,language:'en',format:'json'},
|
|
407
|
+
{},
|
|
408
|
+
{signal:record.controller.signal,operationId:record.operationId}
|
|
409
|
+
);
|
|
410
|
+
if(!this.#currentOperation(record)){
|
|
411
|
+
throw record.terminalError??defineWeatherError(
|
|
412
|
+
new Error(operationMessage(record.kind,'superseded')),
|
|
413
|
+
operationContract(record.kind,'superseded'),
|
|
414
|
+
operationMessage(record.kind,'superseded')
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
this.#finishOperation(record);
|
|
418
|
+
this.#dispatch(
|
|
419
|
+
'locations',
|
|
420
|
+
{
|
|
421
|
+
requestId:record.operationId,
|
|
422
|
+
operation:record.kind,
|
|
423
|
+
locations:result.value
|
|
424
|
+
},
|
|
425
|
+
record.operationId
|
|
426
|
+
);
|
|
427
|
+
return result.value;
|
|
428
|
+
}catch(error){
|
|
429
|
+
const normalized=record.publicError??normalizedOperationError(error,record);
|
|
430
|
+
if(!this.#disposed)this.#publishError(record,normalized);
|
|
431
|
+
throw normalized;
|
|
432
|
+
}finally{
|
|
433
|
+
this.#finishOperation(record);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async load(location,optionsValue={}){
|
|
438
|
+
this.#assertOpen();
|
|
439
|
+
const options=operationOptions(optionsValue);
|
|
440
|
+
let place;
|
|
441
|
+
try{
|
|
442
|
+
place=location instanceof WeatherLocation?location:new WeatherLocation(location);
|
|
443
|
+
}catch(error){
|
|
444
|
+
throw defineWeatherError(
|
|
445
|
+
error,
|
|
446
|
+
OPEN_METEO_WEATHER_ERRORS.weatherLocationInvalid,
|
|
447
|
+
'The weather location is invalid.'
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const {
|
|
451
|
+
temperatureUnit='fahrenheit',
|
|
452
|
+
windSpeedUnit='mph',
|
|
453
|
+
precipitationUnit='inch'
|
|
454
|
+
}=options;
|
|
455
|
+
const record=this.#startOperation('forecast-load',options.signal??null);
|
|
456
|
+
try{
|
|
457
|
+
const result=await this.forecast.fetch(
|
|
458
|
+
{
|
|
459
|
+
latitude:place.latitude,
|
|
460
|
+
longitude:place.longitude,
|
|
461
|
+
timezone:'auto',
|
|
462
|
+
forecast_days:7,
|
|
463
|
+
temperature_unit:temperatureUnit,
|
|
464
|
+
wind_speed_unit:windSpeedUnit,
|
|
465
|
+
precipitation_unit:precipitationUnit,
|
|
466
|
+
current:[
|
|
467
|
+
'temperature_2m',
|
|
468
|
+
'relative_humidity_2m',
|
|
469
|
+
'apparent_temperature',
|
|
470
|
+
'precipitation',
|
|
471
|
+
'weather_code',
|
|
472
|
+
'wind_speed_10m',
|
|
473
|
+
'is_day'
|
|
474
|
+
],
|
|
475
|
+
daily:[
|
|
476
|
+
'weather_code',
|
|
477
|
+
'temperature_2m_max',
|
|
478
|
+
'temperature_2m_min',
|
|
479
|
+
'precipitation_probability_max',
|
|
480
|
+
'sunrise',
|
|
481
|
+
'sunset'
|
|
482
|
+
]
|
|
483
|
+
},
|
|
484
|
+
{location:place},
|
|
485
|
+
{signal:record.controller.signal,operationId:record.operationId}
|
|
486
|
+
);
|
|
487
|
+
if(!this.#currentOperation(record)){
|
|
488
|
+
throw record.terminalError??defineWeatherError(
|
|
489
|
+
new Error(operationMessage(record.kind,'superseded')),
|
|
490
|
+
operationContract(record.kind,'superseded'),
|
|
491
|
+
operationMessage(record.kind,'superseded')
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
this.#finishOperation(record);
|
|
495
|
+
this.#dispatch(
|
|
496
|
+
'weather',
|
|
497
|
+
{
|
|
498
|
+
requestId:record.operationId,
|
|
499
|
+
operation:record.kind,
|
|
500
|
+
weather:result.value
|
|
501
|
+
},
|
|
502
|
+
record.operationId
|
|
503
|
+
);
|
|
504
|
+
return result.value;
|
|
505
|
+
}catch(error){
|
|
506
|
+
const normalized=record.publicError??normalizedOperationError(error,record);
|
|
507
|
+
if(!this.#disposed)this.#publishError(record,normalized);
|
|
508
|
+
throw normalized;
|
|
509
|
+
}finally{
|
|
510
|
+
this.#finishOperation(record);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
emit(type,detail={}){
|
|
515
|
+
this.#assertOpen();
|
|
516
|
+
if(!Object.hasOwn(WEATHER_EVENT_NAMES,type)){
|
|
517
|
+
throw defineWeatherError(
|
|
518
|
+
new TypeError(`Unknown weather event type: ${String(type)}.`),
|
|
519
|
+
OPEN_METEO_WEATHER_ERRORS.weatherEventTypeInvalid,
|
|
520
|
+
'The weather event type is invalid.'
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
const operationId=(typeof detail?.operationId==='string'&&detail.operationId)
|
|
524
|
+
||(typeof detail?.requestId==='string'&&detail.requestId)
|
|
525
|
+
||`${this.#events.instanceId}:emit:${(++this.#operationSequence).toString(36)}`;
|
|
526
|
+
this.#dispatch(type,detail,operationId);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
dispose(){
|
|
530
|
+
if(this.#disposed)return false;
|
|
531
|
+
this.#disposed=true;
|
|
532
|
+
for(const record of [...this.#operations.values()]){
|
|
533
|
+
if(record.settled){
|
|
534
|
+
this.#finishOperation(record);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const error=defineWeatherError(
|
|
538
|
+
new Error('The Open-Meteo weather provider was disposed during an active operation.'),
|
|
539
|
+
OPEN_METEO_WEATHER_ERRORS.providerDisposed,
|
|
540
|
+
'The Open-Meteo weather provider was disposed during an active operation.'
|
|
541
|
+
);
|
|
542
|
+
this.#terminateOperation(record,error);
|
|
543
|
+
this.#finishOperation(record);
|
|
544
|
+
}
|
|
545
|
+
for(const unsubscribe of this.#unsubscribe.splice(0))unsubscribe();
|
|
546
|
+
this.geocoder.dispose();
|
|
547
|
+
this.forecast.dispose();
|
|
548
|
+
return this.#events.dispose();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
destroy(){return this.dispose();}
|
|
12
552
|
}
|
|
13
553
|
|
|
14
|
-
export function mapForecast(raw,location){
|
|
554
|
+
export function mapForecast(raw,location){
|
|
555
|
+
const current=raw.current||{};
|
|
556
|
+
const units=raw.current_units||{};
|
|
557
|
+
const daily=raw.daily||{};
|
|
558
|
+
const dailyUnits=raw.daily_units||{};
|
|
559
|
+
const observation=new WeatherObservation({
|
|
560
|
+
time:current.time,
|
|
561
|
+
temperature:current.temperature_2m,
|
|
562
|
+
apparentTemperature:current.apparent_temperature,
|
|
563
|
+
humidity:current.relative_humidity_2m,
|
|
564
|
+
precipitation:current.precipitation,
|
|
565
|
+
weatherCode:current.weather_code,
|
|
566
|
+
windSpeed:current.wind_speed_10m,
|
|
567
|
+
isDay:current.is_day===1,
|
|
568
|
+
temperatureUnit:units.temperature_2m||'°',
|
|
569
|
+
windUnit:units.wind_speed_10m||''
|
|
570
|
+
});
|
|
571
|
+
const days=Array.from(daily.time||[],function createWeatherDay(date,index){
|
|
572
|
+
return new WeatherDay({
|
|
573
|
+
date,
|
|
574
|
+
weatherCode:daily.weather_code?.[index],
|
|
575
|
+
temperatureMax:daily.temperature_2m_max?.[index],
|
|
576
|
+
temperatureMin:daily.temperature_2m_min?.[index],
|
|
577
|
+
precipitationProbability:daily.precipitation_probability_max?.[index]??0,
|
|
578
|
+
sunrise:daily.sunrise?.[index]||'',
|
|
579
|
+
sunset:daily.sunset?.[index]||'',
|
|
580
|
+
temperatureUnit:dailyUnits.temperature_2m_max||units.temperature_2m||'°'
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
return new WeatherSnapshot({
|
|
584
|
+
location,
|
|
585
|
+
current:observation,
|
|
586
|
+
daily:days,
|
|
587
|
+
source:'Open-Meteo',
|
|
588
|
+
fetchedAt:new Date()
|
|
589
|
+
});
|
|
590
|
+
}
|