arcane-os 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
  4. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
  5. package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
  6. package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
  8. package/browser-runtime/ai/browser-speech.mjs +2 -0
  9. package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
  10. package/browser-runtime/ai/model-controller.mjs +285 -95
  11. package/browser-runtime/ai/speech-worker-client.mjs +247 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
  13. package/browser-runtime/event-manager.mjs +1097 -1
  14. package/docs/architecture.md +2 -2
  15. package/docs/event-manager.md +155 -27
  16. package/docs/reference/README.md +27 -27
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1162 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +6 -3
  21. package/docs/reference/behavioral-testing.md +29 -6
  22. package/docs/reference/cli.md +117 -9
  23. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  24. package/docs/reference/event-manager.md +577 -32
  25. package/docs/reference/inventory/package-api.json +478 -2
  26. package/docs/reference/inventory/runtime-components.json +108 -44
  27. package/docs/reference/inventory/runtime-modules.json +131 -53
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +157 -43
  30. package/docs/reference/runtime-components.md +258 -83
  31. package/docs/reference/runtime-modules.md +613 -77
  32. package/docs/reference/sdk-api.md +1014 -25
  33. package/package.json +5 -4
  34. package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
  35. package/runtime/arcane/components/app-bar.html +34 -13
  36. package/runtime/arcane/components/assistant-panel.html +110 -57
  37. package/runtime/arcane/components/calculator.html +7 -4
  38. package/runtime/arcane/components/chart.html +58 -17
  39. package/runtime/arcane/components/chat.html +606 -136
  40. package/runtime/arcane/components/conversation-view.html +13 -6
  41. package/runtime/arcane/components/dashboard-config.html +96 -59
  42. package/runtime/arcane/components/data-maintenance.html +69 -14
  43. package/runtime/arcane/components/data-view.html +53 -7
  44. package/runtime/arcane/components/directory-picker.html +118 -32
  45. package/runtime/arcane/components/document-inspector.html +47 -10
  46. package/runtime/arcane/components/file-drop.html +81 -35
  47. package/runtime/arcane/components/file-inspector.html +72 -22
  48. package/runtime/arcane/components/file-manager.html +374 -79
  49. package/runtime/arcane/components/integration-settings.html +12 -5
  50. package/runtime/arcane/components/local-ai-status.html +48 -19
  51. package/runtime/arcane/components/markdown-document.html +161 -68
  52. package/runtime/arcane/components/markdown-editor.html +110 -33
  53. package/runtime/arcane/components/media-embed.html +8 -5
  54. package/runtime/arcane/components/modal.html +15 -5
  55. package/runtime/arcane/components/output-panel.html +28 -23
  56. package/runtime/arcane/components/preferences-form.html +22 -4
  57. package/runtime/arcane/components/record-timeline.html +18 -2
  58. package/runtime/arcane/components/relationship-board.html +23 -3
  59. package/runtime/arcane/components/screen-capture.html +10 -4
  60. package/runtime/arcane/components/source-code-viewer.html +76 -9
  61. package/runtime/arcane/components/source-explanation.html +23 -3
  62. package/runtime/arcane/components/speech.html +462 -384
  63. package/runtime/arcane/components/summary-strip.html +22 -11
  64. package/runtime/arcane/components/table.html +39 -21
  65. package/runtime/arcane/components/task-progress.html +79 -21
  66. package/runtime/arcane/components/terminal-workspace.html +7 -4
  67. package/runtime/arcane/components/theme-editor.html +7 -3
  68. package/runtime/arcane/components/unified-inbox.html +9 -4
  69. package/runtime/arcane/components/voice-transcription.html +639 -98
  70. package/runtime/arcane/components/weather-widget.html +5 -3
  71. package/runtime/arcane/components/web-navigator.html +48 -8
  72. package/runtime/arcane/entities/Chat.js +1 -1
  73. package/runtime/arcane/entities/User.js +110 -23
  74. package/runtime/arcane/modules/AI.js +2109 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
  76. package/runtime/arcane/modules/AIRuntimeState.js +109 -52
  77. package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
  78. package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
  79. package/runtime/arcane/modules/CalculatorEngine.js +63 -3
  80. package/runtime/arcane/modules/CommunicationAppController.js +588 -28
  81. package/runtime/arcane/modules/CommunicationHub.js +590 -10
  82. package/runtime/arcane/modules/ComponentContracts.js +470 -0
  83. package/runtime/arcane/modules/ConversationTimebox.js +152 -33
  84. package/runtime/arcane/modules/DBLS.js +40 -7
  85. package/runtime/arcane/modules/DBOPFS.js +35 -11
  86. package/runtime/arcane/modules/DataMaintenance.js +12 -2
  87. package/runtime/arcane/modules/Errors.js +65 -7
  88. package/runtime/arcane/modules/HTMLImport.js +198 -14
  89. package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
  90. package/runtime/arcane/modules/Mail.js +738 -115
  91. package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
  92. package/runtime/arcane/modules/MailTransport.mjs +197 -39
  93. package/runtime/arcane/modules/Ollama.js +36 -1
  94. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
  95. package/runtime/arcane/modules/PreferenceStore.js +367 -33
  96. package/runtime/arcane/modules/RecordReviewStore.js +322 -23
  97. package/runtime/arcane/modules/ScreenCapture.js +1397 -15
  98. package/runtime/arcane/modules/SpeechPlayback.js +438 -41
  99. package/runtime/arcane/modules/TerminalClient.js +277 -12
  100. package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
  101. package/runtime/arcane/modules/ThemeManager.js +39 -7
  102. package/runtime/arcane/modules/TimeGuard.js +131 -20
  103. package/runtime/arcane/modules/WaitForComponent.js +386 -33
  104. package/schemas/arcane-lock.schema.json +2 -2
  105. package/src/cli/main.mjs +435 -9
  106. package/src/event-manager.mjs +1097 -1
  107. package/src/import-map.mjs +21 -3
  108. package/src/index.mjs +13 -0
  109. package/src/installed-sdk-runtime.mjs +112 -0
  110. package/src/mail-api.mjs +22 -0
  111. package/src/mail-credentials.mjs +667 -0
  112. package/src/mail-server.mjs +1769 -0
  113. package/src/mail.mjs +261 -0
  114. package/src/sdk-browser-runtime.mjs +85 -41
  115. package/src/testing-loader.mjs +7 -0
  116. package/src/toolchain.mjs +3 -0
  117. package/src/workspace.mjs +1 -1
@@ -1,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({geocoding:'https://geocoding-api.open-meteo.com/v1/search',forecast:'https://api.open-meteo.com/v1/forecast'});
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
- constructor({geocodingEndpoint=OPEN_METEO_ENDPOINTS.geocoding,forecastEndpoint=OPEN_METEO_ENDPOINTS.forecast,fetchImpl=globalThis.fetch}={}){super();this.geocoder=new ApiModelDatabase({endpoint:geocodingEndpoint,fetchImpl,parser:raw=>Array.from(raw.results||[],item=>new WeatherLocation({id:item.id,name:item.name,region:item.admin1||'',country:item.country||'',latitude:item.latitude,longitude:item.longitude,timezone:item.timezone||'auto'}))});this.forecast=new ApiModelDatabase({endpoint:forecastEndpoint,fetchImpl,parser:(raw,{context})=>mapForecast(raw,context.location)});for(const model of [this.geocoder,this.forecast]){model.addEventListener('api-model-request',event=>this.emit('request',event.detail));model.addEventListener('api-model-error',event=>this.emit('error',event.detail));}}
8
- setEndpoints({geocoding,forecast}={}){if(geocoding)this.geocoder.setEndpoint(geocoding);if(forecast)this.forecast.setEndpoint(forecast);return {geocoding:this.geocoder.endpoint,forecast:this.forecast.endpoint};}
9
- async search(query){const name=String(query||'').trim();if(name.length<2)throw new TypeError('Enter at least two characters for a location search.');const record=await this.geocoder.fetch({name,count:8,language:'en',format:'json'});this.emit('locations',{locations:record.value});return record.value;}
10
- async load(location,{temperatureUnit='fahrenheit',windSpeedUnit='mph',precipitationUnit='inch'}={}){const place=location instanceof WeatherLocation?location:new WeatherLocation(location);const record=await this.forecast.fetch({latitude:place.latitude,longitude:place.longitude,timezone:'auto',forecast_days:7,temperature_unit:temperatureUnit,wind_speed_unit:windSpeedUnit,precipitation_unit:precipitationUnit,current:['temperature_2m','relative_humidity_2m','apparent_temperature','precipitation','weather_code','wind_speed_10m','is_day'],daily:['weather_code','temperature_2m_max','temperature_2m_min','precipitation_probability_max','sunrise','sunset']},{location:place});this.emit('weather',{weather:record.value});return record.value;}
11
- emit(type,detail){this.dispatchEvent(new CustomEvent(`weather-${type}`,{detail}));}
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){const current=raw.current||{},units=raw.current_units||{},daily=raw.daily||{},dailyUnits=raw.daily_units||{};const observation=new WeatherObservation({time:current.time,temperature:current.temperature_2m,apparentTemperature:current.apparent_temperature,humidity:current.relative_humidity_2m,precipitation:current.precipitation,weatherCode:current.weather_code,windSpeed:current.wind_speed_10m,isDay:current.is_day===1,temperatureUnit:units.temperature_2m||'°',windUnit:units.wind_speed_10m||''});const days=Array.from(daily.time||[],(date,index)=>new WeatherDay({date,weatherCode:daily.weather_code?.[index],temperatureMax:daily.temperature_2m_max?.[index],temperatureMin:daily.temperature_2m_min?.[index],precipitationProbability:daily.precipitation_probability_max?.[index]??0,sunrise:daily.sunrise?.[index]||'',sunset:daily.sunset?.[index]||'',temperatureUnit:dailyUnits.temperature_2m_max||units.temperature_2m||'°'}));return new WeatherSnapshot({location,current:observation,daily:days,source:'Open-Meteo',fetchedAt:new Date()});}
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
+ }