@maplibre-yaml/core 0.1.0-alpha.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/LICENSE.md +21 -0
- package/dist/components/index.d.ts +106 -0
- package/dist/components/index.js +4332 -0
- package/dist/components/index.js.map +1 -0
- package/dist/index.d.ts +2717 -0
- package/dist/index.js +4305 -0
- package/dist/index.js.map +1 -0
- package/dist/map-renderer-RQc5_bdo.d.ts +149 -0
- package/dist/map.schema-EnZRrtIh.d.ts +68122 -0
- package/dist/schemas/index.d.ts +3151 -0
- package/dist/schemas/index.js +710 -0
- package/dist/schemas/index.js.map +1 -0
- package/package.json +79 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2717 @@
|
|
|
1
|
+
import { RootSchema } from './schemas/index.js';
|
|
2
|
+
export { BlockSchema, ChapterActionSchema, ChapterLayersSchema, ChapterSchema, ColorOrExpressionSchema, ColorSchema, ContentBlockSchema, ContentElementSchema, ContentItemSchema, ExpressionSchema, GeoJSONSourceSchema, GlobalConfigSchema, ImageSourceSchema, LatitudeSchema, LayerSourceSchema, LngLatBoundsSchema, LngLatSchema, LoadingConfigSchema, LongitudeSchema, MixedBlockSchema, NumberOrExpressionSchema, PageSchema, RasterSourceSchema, ScrollytellingBlockSchema, StreamConfigSchema, ValidTagNames, VectorSourceSchema, VideoSourceSchema, ZoomLevelSchema } from './schemas/index.js';
|
|
3
|
+
import { L as LayerSchema, P as PopupContentSchema, C as ControlsConfigSchema } from './map.schema-EnZRrtIh.js';
|
|
4
|
+
export { h as BackgroundLayerSchema, B as BaseLayerPropertiesSchema, d as CircleLayerSchema, k as ControlPositionSchema, f as FillExtrusionLayerSchema, F as FillLayerSchema, H as HeatmapLayerSchema, g as HillshadeLayerSchema, I as InteractiveConfigSchema, j as LayerOrReferenceSchema, i as LayerReferenceSchema, a as LegendConfigSchema, c as LegendItemSchema, e as LineLayerSchema, l as MapBlockSchema, M as MapConfigSchema, m as MapFullPageBlockSchema, b as PopupContentItemSchema, R as RasterLayerSchema, S as SymbolLayerSchema } from './map.schema-EnZRrtIh.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export { L as LegendBuilder, M as MapRenderer, b as MapRendererEvents, a as MapRendererOptions } from './map-renderer-RQc5_bdo.js';
|
|
7
|
+
import { Map, LngLat } from 'maplibre-gl';
|
|
8
|
+
import { FeatureCollection } from 'geojson';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @file YAML parser for MapLibre configuration files
|
|
12
|
+
* @module @maplibre-yaml/core/parser
|
|
13
|
+
*
|
|
14
|
+
* @description
|
|
15
|
+
* This module provides YAML parsing, schema validation, and reference resolution
|
|
16
|
+
* for MapLibre YAML configuration files. It converts YAML strings into validated
|
|
17
|
+
* TypeScript objects ready for rendering.
|
|
18
|
+
*
|
|
19
|
+
* ## Features
|
|
20
|
+
*
|
|
21
|
+
* - **YAML Parsing**: Converts YAML strings to JavaScript objects
|
|
22
|
+
* - **Schema Validation**: Validates against Zod schemas with detailed error messages
|
|
23
|
+
* - **Reference Resolution**: Resolves `$ref` pointers to global layers and sources
|
|
24
|
+
* - **Error Formatting**: Transforms Zod errors into user-friendly messages with paths
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* Basic parsing
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import { YAMLParser } from '@maplibre-yaml/core/parser';
|
|
30
|
+
*
|
|
31
|
+
* const yaml = `
|
|
32
|
+
* pages:
|
|
33
|
+
* - path: "/"
|
|
34
|
+
* title: "My Map"
|
|
35
|
+
* blocks:
|
|
36
|
+
* - type: map
|
|
37
|
+
* id: main
|
|
38
|
+
* config:
|
|
39
|
+
* center: [0, 0]
|
|
40
|
+
* zoom: 2
|
|
41
|
+
* mapStyle: "https://example.com/style.json"
|
|
42
|
+
* `;
|
|
43
|
+
*
|
|
44
|
+
* const config = YAMLParser.parse(yaml);
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* Safe parsing with error handling
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const result = YAMLParser.safeParse(yaml);
|
|
51
|
+
* if (result.success) {
|
|
52
|
+
* console.log('Valid config:', result.data);
|
|
53
|
+
* } else {
|
|
54
|
+
* result.errors.forEach(err => {
|
|
55
|
+
* console.error(`${err.path}: ${err.message}`);
|
|
56
|
+
* });
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* Reference resolution
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const yaml = `
|
|
64
|
+
* layers:
|
|
65
|
+
* myLayer:
|
|
66
|
+
* id: shared
|
|
67
|
+
* type: circle
|
|
68
|
+
* source: { type: geojson, data: {...} }
|
|
69
|
+
*
|
|
70
|
+
* pages:
|
|
71
|
+
* - path: "/"
|
|
72
|
+
* blocks:
|
|
73
|
+
* - type: map
|
|
74
|
+
* layers:
|
|
75
|
+
* - $ref: "#/layers/myLayer"
|
|
76
|
+
* `;
|
|
77
|
+
*
|
|
78
|
+
* const config = YAMLParser.parse(yaml);
|
|
79
|
+
* // References are automatically resolved
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Type alias for the root configuration object
|
|
85
|
+
*
|
|
86
|
+
* @remarks
|
|
87
|
+
* Inferred from the RootSchema Zod schema
|
|
88
|
+
*/
|
|
89
|
+
type RootConfig = z.infer<typeof RootSchema>;
|
|
90
|
+
/**
|
|
91
|
+
* Error information for a single validation or parsing error
|
|
92
|
+
*
|
|
93
|
+
* @property path - JSON path to the error location (e.g., "pages[0].blocks[1].config.center")
|
|
94
|
+
* @property message - Human-readable error description
|
|
95
|
+
* @property line - Optional line number in the YAML file where error occurred
|
|
96
|
+
* @property column - Optional column number in the YAML file where error occurred
|
|
97
|
+
*/
|
|
98
|
+
interface ParseError {
|
|
99
|
+
path: string;
|
|
100
|
+
message: string;
|
|
101
|
+
line?: number;
|
|
102
|
+
column?: number;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Result object returned by safeParse operations
|
|
106
|
+
*
|
|
107
|
+
* @property success - Whether parsing and validation succeeded
|
|
108
|
+
* @property data - Validated configuration object (only present if success is true)
|
|
109
|
+
* @property errors - Array of errors (only present if success is false)
|
|
110
|
+
*/
|
|
111
|
+
interface ParseResult {
|
|
112
|
+
success: boolean;
|
|
113
|
+
data?: RootConfig;
|
|
114
|
+
errors: ParseError[];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* YAML parser with schema validation and reference resolution
|
|
118
|
+
*
|
|
119
|
+
* @remarks
|
|
120
|
+
* This class provides static methods for parsing YAML configuration files,
|
|
121
|
+
* validating them against schemas, and resolving references between configuration
|
|
122
|
+
* sections. All methods are static as the parser maintains no internal state.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* Parse and validate YAML (throws on error)
|
|
126
|
+
* ```typescript
|
|
127
|
+
* const config = YAMLParser.parse(yamlString);
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* Safe parse (returns result object)
|
|
132
|
+
* ```typescript
|
|
133
|
+
* const result = YAMLParser.safeParse(yamlString);
|
|
134
|
+
* if (!result.success) {
|
|
135
|
+
* console.error('Validation errors:', result.errors);
|
|
136
|
+
* }
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
declare class YAMLParser {
|
|
140
|
+
/**
|
|
141
|
+
* Parse YAML string and validate against schema
|
|
142
|
+
*
|
|
143
|
+
* @param yaml - YAML string to parse
|
|
144
|
+
* @returns Validated configuration object
|
|
145
|
+
* @throws {Error} If YAML syntax is invalid
|
|
146
|
+
* @throws {ZodError} If validation fails
|
|
147
|
+
*
|
|
148
|
+
* @remarks
|
|
149
|
+
* This method parses the YAML, validates it against the RootSchema,
|
|
150
|
+
* resolves all references, and returns the validated config. If any
|
|
151
|
+
* step fails, it throws an error.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* try {
|
|
156
|
+
* const config = YAMLParser.parse(yamlString);
|
|
157
|
+
* console.log('Valid config:', config);
|
|
158
|
+
* } catch (error) {
|
|
159
|
+
* console.error('Parse error:', error.message);
|
|
160
|
+
* }
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
static parse(yaml: string): RootConfig;
|
|
164
|
+
/**
|
|
165
|
+
* Parse YAML string and validate, returning a result object
|
|
166
|
+
*
|
|
167
|
+
* @param yaml - YAML string to parse
|
|
168
|
+
* @returns Result object with success flag and either data or errors
|
|
169
|
+
*
|
|
170
|
+
* @remarks
|
|
171
|
+
* This is the non-throwing version of {@link parse}. Instead of throwing
|
|
172
|
+
* errors, it returns a result object that indicates success or failure.
|
|
173
|
+
* Use this when you want to handle errors gracefully without try/catch.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```typescript
|
|
177
|
+
* const result = YAMLParser.safeParse(yamlString);
|
|
178
|
+
* if (result.success) {
|
|
179
|
+
* console.log('Config:', result.data);
|
|
180
|
+
* } else {
|
|
181
|
+
* result.errors.forEach(err => {
|
|
182
|
+
* console.error(`Error at ${err.path}: ${err.message}`);
|
|
183
|
+
* });
|
|
184
|
+
* }
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
static safeParse(yaml: string): ParseResult;
|
|
188
|
+
/**
|
|
189
|
+
* Validate a JavaScript object against the schema
|
|
190
|
+
*
|
|
191
|
+
* @param config - JavaScript object to validate
|
|
192
|
+
* @returns Validated configuration object
|
|
193
|
+
* @throws {ZodError} If validation fails
|
|
194
|
+
*
|
|
195
|
+
* @remarks
|
|
196
|
+
* This method bypasses YAML parsing and directly validates a JavaScript object.
|
|
197
|
+
* Useful when you already have a parsed object (e.g., from JSON.parse) and just
|
|
198
|
+
* want to validate and resolve references.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* const jsConfig = JSON.parse(jsonString);
|
|
203
|
+
* const validated = YAMLParser.validate(jsConfig);
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
static validate(config: unknown): RootConfig;
|
|
207
|
+
/**
|
|
208
|
+
* Resolve $ref references to global layers and sources
|
|
209
|
+
*
|
|
210
|
+
* @param config - Configuration object with potential references
|
|
211
|
+
* @returns Configuration with all references resolved
|
|
212
|
+
* @throws {Error} If a reference cannot be resolved
|
|
213
|
+
*
|
|
214
|
+
* @remarks
|
|
215
|
+
* References use JSON Pointer-like syntax: `#/layers/layerName` or `#/sources/sourceName`.
|
|
216
|
+
* This method walks the configuration tree, finds all objects with a `$ref` property,
|
|
217
|
+
* looks up the referenced item in `config.layers` or `config.sources`, and replaces
|
|
218
|
+
* the reference object with the actual item.
|
|
219
|
+
*
|
|
220
|
+
* ## Reference Syntax
|
|
221
|
+
*
|
|
222
|
+
* - `#/layers/myLayer` - Reference to a layer in the global `layers` section
|
|
223
|
+
* - `#/sources/mySource` - Reference to a source in the global `sources` section
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* const config = {
|
|
228
|
+
* layers: {
|
|
229
|
+
* myLayer: { id: 'layer1', type: 'circle', ... }
|
|
230
|
+
* },
|
|
231
|
+
* pages: [{
|
|
232
|
+
* blocks: [{
|
|
233
|
+
* type: 'map',
|
|
234
|
+
* layers: [{ $ref: '#/layers/myLayer' }]
|
|
235
|
+
* }]
|
|
236
|
+
* }]
|
|
237
|
+
* };
|
|
238
|
+
*
|
|
239
|
+
* const resolved = YAMLParser.resolveReferences(config);
|
|
240
|
+
* // resolved.pages[0].blocks[0].layers[0] now contains the full layer object
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
static resolveReferences(config: RootConfig): RootConfig;
|
|
244
|
+
/**
|
|
245
|
+
* Format Zod validation errors into user-friendly messages
|
|
246
|
+
*
|
|
247
|
+
* @param error - Zod validation error
|
|
248
|
+
* @returns Array of formatted error objects
|
|
249
|
+
*
|
|
250
|
+
* @remarks
|
|
251
|
+
* This method transforms Zod's internal error format into human-readable
|
|
252
|
+
* messages with clear paths and descriptions. It handles various Zod error
|
|
253
|
+
* types and provides appropriate messages for each.
|
|
254
|
+
*
|
|
255
|
+
* ## Error Type Handling
|
|
256
|
+
*
|
|
257
|
+
* - `invalid_type`: Type mismatch (e.g., expected number, got string)
|
|
258
|
+
* - `invalid_union_discriminator`: Invalid discriminator for union types
|
|
259
|
+
* - `invalid_union`: None of the union options matched
|
|
260
|
+
* - `too_small`: Value below minimum (arrays, strings, numbers)
|
|
261
|
+
* - `too_big`: Value above maximum
|
|
262
|
+
* - `invalid_string`: String format validation failed
|
|
263
|
+
* - `custom`: Custom validation refinement failed
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```typescript
|
|
267
|
+
* try {
|
|
268
|
+
* RootSchema.parse(invalidConfig);
|
|
269
|
+
* } catch (error) {
|
|
270
|
+
* if (error instanceof ZodError) {
|
|
271
|
+
* const formatted = YAMLParser.formatZodErrors(error);
|
|
272
|
+
* formatted.forEach(err => {
|
|
273
|
+
* console.error(`${err.path}: ${err.message}`);
|
|
274
|
+
* });
|
|
275
|
+
* }
|
|
276
|
+
* }
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
private static formatZodErrors;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Convenience function to parse YAML config
|
|
283
|
+
*
|
|
284
|
+
* @param yaml - YAML string to parse
|
|
285
|
+
* @returns Validated configuration object
|
|
286
|
+
* @throws {Error} If parsing or validation fails
|
|
287
|
+
*
|
|
288
|
+
* @remarks
|
|
289
|
+
* This is an alias for {@link YAMLParser.parse} for convenient imports.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```typescript
|
|
293
|
+
* import { parseYAMLConfig } from '@maplibre-yaml/core/parser';
|
|
294
|
+
* const config = parseYAMLConfig(yamlString);
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
declare const parseYAMLConfig: typeof YAMLParser.parse;
|
|
298
|
+
/**
|
|
299
|
+
* Convenience function to safely parse YAML config
|
|
300
|
+
*
|
|
301
|
+
* @param yaml - YAML string to parse
|
|
302
|
+
* @returns Result object with success flag and data or errors
|
|
303
|
+
*
|
|
304
|
+
* @remarks
|
|
305
|
+
* This is an alias for {@link YAMLParser.safeParse} for convenient imports.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```typescript
|
|
309
|
+
* import { safeParseYAMLConfig } from '@maplibre-yaml/core/parser';
|
|
310
|
+
* const result = safeParseYAMLConfig(yamlString);
|
|
311
|
+
* if (result.success) {
|
|
312
|
+
* console.log(result.data);
|
|
313
|
+
* }
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
declare const safeParseYAMLConfig: typeof YAMLParser.safeParse;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @file Layer manager for MapLibre map layers
|
|
320
|
+
* @module @maplibre-yaml/core/renderer
|
|
321
|
+
*/
|
|
322
|
+
|
|
323
|
+
type Layer = z.infer<typeof LayerSchema>;
|
|
324
|
+
/**
|
|
325
|
+
* Callbacks for layer data loading events
|
|
326
|
+
*/
|
|
327
|
+
interface LayerManagerCallbacks {
|
|
328
|
+
onDataLoading?: (layerId: string) => void;
|
|
329
|
+
onDataLoaded?: (layerId: string, featureCount: number) => void;
|
|
330
|
+
onDataError?: (layerId: string, error: Error) => void;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Manages map layers and their data sources
|
|
334
|
+
*/
|
|
335
|
+
declare class LayerManager {
|
|
336
|
+
private map;
|
|
337
|
+
private callbacks;
|
|
338
|
+
private dataFetcher;
|
|
339
|
+
private pollingManager;
|
|
340
|
+
private streamManager;
|
|
341
|
+
private dataMerger;
|
|
342
|
+
private loadingManager;
|
|
343
|
+
private sourceData;
|
|
344
|
+
private layerToSource;
|
|
345
|
+
private refreshIntervals;
|
|
346
|
+
private abortControllers;
|
|
347
|
+
constructor(map: Map, callbacks?: LayerManagerCallbacks);
|
|
348
|
+
addLayer(layer: Layer): Promise<void>;
|
|
349
|
+
private addSource;
|
|
350
|
+
private addGeoJSONSourceFromURL;
|
|
351
|
+
/**
|
|
352
|
+
* Setup polling and/or streaming for a GeoJSON source
|
|
353
|
+
*/
|
|
354
|
+
private setupDataUpdates;
|
|
355
|
+
/**
|
|
356
|
+
* Handle incoming data updates with merge strategy
|
|
357
|
+
*/
|
|
358
|
+
private handleDataUpdate;
|
|
359
|
+
/**
|
|
360
|
+
* Pause data refresh for a layer (polling)
|
|
361
|
+
*/
|
|
362
|
+
pauseRefresh(layerId: string): void;
|
|
363
|
+
/**
|
|
364
|
+
* Resume data refresh for a layer (polling)
|
|
365
|
+
*/
|
|
366
|
+
resumeRefresh(layerId: string): void;
|
|
367
|
+
/**
|
|
368
|
+
* Force immediate refresh for a layer (polling)
|
|
369
|
+
*/
|
|
370
|
+
refreshNow(layerId: string): Promise<void>;
|
|
371
|
+
/**
|
|
372
|
+
* Disconnect streaming connection for a layer
|
|
373
|
+
*/
|
|
374
|
+
disconnectStream(layerId: string): void;
|
|
375
|
+
removeLayer(layerId: string): void;
|
|
376
|
+
setVisibility(layerId: string, visible: boolean): void;
|
|
377
|
+
updateData(layerId: string, data: GeoJSON.GeoJSON): void;
|
|
378
|
+
/**
|
|
379
|
+
* @deprecated Legacy refresh method - use PollingManager instead
|
|
380
|
+
*/
|
|
381
|
+
startRefreshInterval(layer: Layer): void;
|
|
382
|
+
stopRefreshInterval(layerId: string): void;
|
|
383
|
+
clearAllIntervals(): void;
|
|
384
|
+
destroy(): void;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* @file Event handler for map layer interactions
|
|
389
|
+
* @module @maplibre-yaml/core/renderer
|
|
390
|
+
*/
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Callbacks for interactive events
|
|
394
|
+
*/
|
|
395
|
+
interface EventHandlerCallbacks {
|
|
396
|
+
onClick?: (layerId: string, feature: any, lngLat: LngLat) => void;
|
|
397
|
+
onHover?: (layerId: string, feature: any, lngLat: LngLat) => void;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* @file Popup HTML builder for map features
|
|
402
|
+
* @module @maplibre-yaml/core/renderer
|
|
403
|
+
*/
|
|
404
|
+
|
|
405
|
+
type PopupContent = z.infer<typeof PopupContentSchema>;
|
|
406
|
+
/**
|
|
407
|
+
* Builds popup HTML from configuration and feature properties
|
|
408
|
+
*/
|
|
409
|
+
declare class PopupBuilder {
|
|
410
|
+
/**
|
|
411
|
+
* Build HTML string from popup content config and feature properties
|
|
412
|
+
*/
|
|
413
|
+
build(content: PopupContent, properties: Record<string, any>): string;
|
|
414
|
+
/**
|
|
415
|
+
* Build a single content item
|
|
416
|
+
*/
|
|
417
|
+
private buildItem;
|
|
418
|
+
/**
|
|
419
|
+
* Format a number according to format string
|
|
420
|
+
*/
|
|
421
|
+
private formatNumber;
|
|
422
|
+
/**
|
|
423
|
+
* Escape HTML to prevent XSS
|
|
424
|
+
*/
|
|
425
|
+
private escapeHtml;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* @file Controls manager for map controls
|
|
430
|
+
* @module @maplibre-yaml/core/renderer
|
|
431
|
+
*/
|
|
432
|
+
|
|
433
|
+
type ControlsConfig = z.infer<typeof ControlsConfigSchema>;
|
|
434
|
+
/**
|
|
435
|
+
* Manages MapLibre map controls (navigation, geolocate, scale, etc.)
|
|
436
|
+
*/
|
|
437
|
+
declare class ControlsManager {
|
|
438
|
+
private map;
|
|
439
|
+
private addedControls;
|
|
440
|
+
constructor(map: Map);
|
|
441
|
+
/**
|
|
442
|
+
* Add controls to the map based on configuration
|
|
443
|
+
*/
|
|
444
|
+
addControls(config: ControlsConfig): void;
|
|
445
|
+
/**
|
|
446
|
+
* Remove all controls from the map
|
|
447
|
+
*/
|
|
448
|
+
removeAllControls(): void;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* @file TTL-based in-memory cache for fetched data
|
|
453
|
+
* @module @maplibre-yaml/core/data
|
|
454
|
+
*/
|
|
455
|
+
/**
|
|
456
|
+
* Configuration options for MemoryCache
|
|
457
|
+
*/
|
|
458
|
+
interface CacheConfig {
|
|
459
|
+
/**
|
|
460
|
+
* Maximum number of entries to store
|
|
461
|
+
* @default 100
|
|
462
|
+
*/
|
|
463
|
+
maxSize: number;
|
|
464
|
+
/**
|
|
465
|
+
* Default time-to-live in milliseconds
|
|
466
|
+
* @default 300000 (5 minutes)
|
|
467
|
+
*/
|
|
468
|
+
defaultTTL: number;
|
|
469
|
+
/**
|
|
470
|
+
* Whether to use conditional requests with ETag/Last-Modified headers
|
|
471
|
+
* @default true
|
|
472
|
+
*/
|
|
473
|
+
useConditionalRequests: boolean;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Cache entry containing data and metadata
|
|
477
|
+
*/
|
|
478
|
+
interface CacheEntry<T = unknown> {
|
|
479
|
+
/** Cached data */
|
|
480
|
+
data: T;
|
|
481
|
+
/** Timestamp when cached (milliseconds) */
|
|
482
|
+
timestamp: number;
|
|
483
|
+
/** Time-to-live for this entry in milliseconds (overrides default) */
|
|
484
|
+
ttl?: number;
|
|
485
|
+
/** ETag header from HTTP response */
|
|
486
|
+
etag?: string;
|
|
487
|
+
/** Last-Modified header from HTTP response */
|
|
488
|
+
lastModified?: string;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Cache statistics
|
|
492
|
+
*/
|
|
493
|
+
interface CacheStats {
|
|
494
|
+
/** Number of entries currently in cache */
|
|
495
|
+
size: number;
|
|
496
|
+
/** Total number of cache hits */
|
|
497
|
+
hits: number;
|
|
498
|
+
/** Total number of cache misses */
|
|
499
|
+
misses: number;
|
|
500
|
+
/** Hit rate as percentage (0-100) */
|
|
501
|
+
hitRate: number;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* TTL-based in-memory cache with LRU eviction.
|
|
505
|
+
*
|
|
506
|
+
* @remarks
|
|
507
|
+
* Features:
|
|
508
|
+
* - LRU (Least Recently Used) eviction when at capacity
|
|
509
|
+
* - Respects HTTP cache headers (ETag, Last-Modified)
|
|
510
|
+
* - Configurable per-entry TTL
|
|
511
|
+
* - Support for conditional requests (If-None-Match, If-Modified-Since)
|
|
512
|
+
* - Automatic expiration checking on get operations
|
|
513
|
+
*
|
|
514
|
+
* @example
|
|
515
|
+
* ```typescript
|
|
516
|
+
* const cache = new MemoryCache<GeoJSON.FeatureCollection>({
|
|
517
|
+
* maxSize: 100,
|
|
518
|
+
* defaultTTL: 300000, // 5 minutes
|
|
519
|
+
* });
|
|
520
|
+
*
|
|
521
|
+
* // Store data with ETag
|
|
522
|
+
* cache.set('https://example.com/data.json', {
|
|
523
|
+
* data: geojsonData,
|
|
524
|
+
* timestamp: Date.now(),
|
|
525
|
+
* etag: '"abc123"',
|
|
526
|
+
* });
|
|
527
|
+
*
|
|
528
|
+
* // Retrieve (returns null if expired or missing)
|
|
529
|
+
* const entry = cache.get('https://example.com/data.json');
|
|
530
|
+
* if (entry) {
|
|
531
|
+
* console.log('Cache hit:', entry.data);
|
|
532
|
+
* }
|
|
533
|
+
*
|
|
534
|
+
* // Get conditional headers for HTTP request
|
|
535
|
+
* const headers = cache.getConditionalHeaders('https://example.com/data.json');
|
|
536
|
+
* // headers = { 'If-None-Match': '"abc123"' }
|
|
537
|
+
* ```
|
|
538
|
+
*
|
|
539
|
+
* @typeParam T - Type of cached data
|
|
540
|
+
*/
|
|
541
|
+
declare class MemoryCache<T = unknown> {
|
|
542
|
+
private static readonly DEFAULT_CONFIG;
|
|
543
|
+
private config;
|
|
544
|
+
private cache;
|
|
545
|
+
private accessOrder;
|
|
546
|
+
private stats;
|
|
547
|
+
/**
|
|
548
|
+
* Create a new MemoryCache instance
|
|
549
|
+
*
|
|
550
|
+
* @param config - Cache configuration options
|
|
551
|
+
*/
|
|
552
|
+
constructor(config?: Partial<CacheConfig>);
|
|
553
|
+
/**
|
|
554
|
+
* Retrieve a cache entry
|
|
555
|
+
*
|
|
556
|
+
* @remarks
|
|
557
|
+
* - Returns null if key doesn't exist
|
|
558
|
+
* - Returns null if entry has expired (and removes it)
|
|
559
|
+
* - Updates access order for LRU
|
|
560
|
+
* - Updates cache statistics
|
|
561
|
+
*
|
|
562
|
+
* @param key - Cache key (typically a URL)
|
|
563
|
+
* @returns Cache entry or null if not found/expired
|
|
564
|
+
*/
|
|
565
|
+
get(key: string): CacheEntry<T> | null;
|
|
566
|
+
/**
|
|
567
|
+
* Check if a key exists in cache (without checking expiration)
|
|
568
|
+
*
|
|
569
|
+
* @param key - Cache key
|
|
570
|
+
* @returns True if key exists in cache
|
|
571
|
+
*/
|
|
572
|
+
has(key: string): boolean;
|
|
573
|
+
/**
|
|
574
|
+
* Store a cache entry
|
|
575
|
+
*
|
|
576
|
+
* @remarks
|
|
577
|
+
* - Evicts least recently used entries if at capacity
|
|
578
|
+
* - Updates access order
|
|
579
|
+
*
|
|
580
|
+
* @param key - Cache key (typically a URL)
|
|
581
|
+
* @param entry - Cache entry to store
|
|
582
|
+
*/
|
|
583
|
+
set(key: string, entry: CacheEntry<T>): void;
|
|
584
|
+
/**
|
|
585
|
+
* Delete a cache entry
|
|
586
|
+
*
|
|
587
|
+
* @param key - Cache key
|
|
588
|
+
* @returns True if entry was deleted, false if it didn't exist
|
|
589
|
+
*/
|
|
590
|
+
delete(key: string): boolean;
|
|
591
|
+
/**
|
|
592
|
+
* Clear all cache entries and reset statistics
|
|
593
|
+
*/
|
|
594
|
+
clear(): void;
|
|
595
|
+
/**
|
|
596
|
+
* Remove expired entries from cache
|
|
597
|
+
*
|
|
598
|
+
* @remarks
|
|
599
|
+
* Iterates through all entries and removes those that have exceeded their TTL.
|
|
600
|
+
* This is useful for periodic cleanup.
|
|
601
|
+
*
|
|
602
|
+
* @returns Number of entries removed
|
|
603
|
+
*/
|
|
604
|
+
prune(): number;
|
|
605
|
+
/**
|
|
606
|
+
* Get cache statistics
|
|
607
|
+
*
|
|
608
|
+
* @returns Current cache statistics including hit rate
|
|
609
|
+
*/
|
|
610
|
+
getStats(): CacheStats;
|
|
611
|
+
/**
|
|
612
|
+
* Get conditional request headers for HTTP caching
|
|
613
|
+
*
|
|
614
|
+
* @remarks
|
|
615
|
+
* Returns appropriate If-None-Match and/or If-Modified-Since headers
|
|
616
|
+
* based on cached entry metadata. Returns empty object if:
|
|
617
|
+
* - Key doesn't exist in cache
|
|
618
|
+
* - Entry has expired
|
|
619
|
+
* - useConditionalRequests is false
|
|
620
|
+
*
|
|
621
|
+
* @param key - Cache key
|
|
622
|
+
* @returns Object with conditional headers (may be empty)
|
|
623
|
+
*
|
|
624
|
+
* @example
|
|
625
|
+
* ```typescript
|
|
626
|
+
* const headers = cache.getConditionalHeaders(url);
|
|
627
|
+
* const response = await fetch(url, { headers });
|
|
628
|
+
* if (response.status === 304) {
|
|
629
|
+
* // Use cached data
|
|
630
|
+
* }
|
|
631
|
+
* ```
|
|
632
|
+
*/
|
|
633
|
+
getConditionalHeaders(key: string): Record<string, string>;
|
|
634
|
+
/**
|
|
635
|
+
* Update the last access time for an entry
|
|
636
|
+
*
|
|
637
|
+
* @remarks
|
|
638
|
+
* Useful for keeping an entry "fresh" without modifying its data.
|
|
639
|
+
* Updates the access order for LRU.
|
|
640
|
+
*
|
|
641
|
+
* @param key - Cache key
|
|
642
|
+
*/
|
|
643
|
+
touch(key: string): void;
|
|
644
|
+
/**
|
|
645
|
+
* Update access order for LRU tracking
|
|
646
|
+
*
|
|
647
|
+
* @param key - Cache key
|
|
648
|
+
*/
|
|
649
|
+
private updateAccessOrder;
|
|
650
|
+
/**
|
|
651
|
+
* Remove a key from access order array
|
|
652
|
+
*
|
|
653
|
+
* @param key - Cache key
|
|
654
|
+
*/
|
|
655
|
+
private removeFromAccessOrder;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* @file HTTP data fetcher with caching and retry support
|
|
660
|
+
* @module @maplibre-yaml/core/data
|
|
661
|
+
*/
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Configuration for DataFetcher
|
|
665
|
+
*/
|
|
666
|
+
interface FetcherConfig {
|
|
667
|
+
/**
|
|
668
|
+
* Cache configuration
|
|
669
|
+
*/
|
|
670
|
+
cache: {
|
|
671
|
+
/** Whether caching is enabled */
|
|
672
|
+
enabled: boolean;
|
|
673
|
+
/** Default TTL in milliseconds */
|
|
674
|
+
defaultTTL: number;
|
|
675
|
+
/** Maximum number of cached entries */
|
|
676
|
+
maxSize: number;
|
|
677
|
+
};
|
|
678
|
+
/**
|
|
679
|
+
* Retry configuration
|
|
680
|
+
*/
|
|
681
|
+
retry: {
|
|
682
|
+
/** Whether retry is enabled */
|
|
683
|
+
enabled: boolean;
|
|
684
|
+
/** Maximum number of retry attempts */
|
|
685
|
+
maxRetries: number;
|
|
686
|
+
/** Initial delay in milliseconds */
|
|
687
|
+
initialDelay: number;
|
|
688
|
+
/** Maximum delay in milliseconds */
|
|
689
|
+
maxDelay: number;
|
|
690
|
+
};
|
|
691
|
+
/**
|
|
692
|
+
* Request timeout in milliseconds
|
|
693
|
+
* @default 30000
|
|
694
|
+
*/
|
|
695
|
+
timeout: number;
|
|
696
|
+
/**
|
|
697
|
+
* Default headers to include in all requests
|
|
698
|
+
*/
|
|
699
|
+
defaultHeaders: Record<string, string>;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Options for individual fetch requests
|
|
703
|
+
*/
|
|
704
|
+
interface FetchOptions {
|
|
705
|
+
/**
|
|
706
|
+
* Custom TTL for this request (overrides default)
|
|
707
|
+
*/
|
|
708
|
+
ttl?: number;
|
|
709
|
+
/**
|
|
710
|
+
* Skip cache and force fresh fetch
|
|
711
|
+
* @default false
|
|
712
|
+
*/
|
|
713
|
+
skipCache?: boolean;
|
|
714
|
+
/**
|
|
715
|
+
* AbortSignal for request cancellation
|
|
716
|
+
*/
|
|
717
|
+
signal?: AbortSignal;
|
|
718
|
+
/**
|
|
719
|
+
* Additional headers for this request
|
|
720
|
+
*/
|
|
721
|
+
headers?: Record<string, string>;
|
|
722
|
+
/**
|
|
723
|
+
* Callback before each retry attempt
|
|
724
|
+
*/
|
|
725
|
+
onRetry?: (attempt: number, delay: number, error: Error) => void;
|
|
726
|
+
/**
|
|
727
|
+
* Callback when fetch starts
|
|
728
|
+
*/
|
|
729
|
+
onStart?: () => void;
|
|
730
|
+
/**
|
|
731
|
+
* Callback when fetch completes successfully
|
|
732
|
+
*/
|
|
733
|
+
onComplete?: (data: FeatureCollection, fromCache: boolean) => void;
|
|
734
|
+
/**
|
|
735
|
+
* Callback when fetch fails
|
|
736
|
+
*/
|
|
737
|
+
onError?: (error: Error) => void;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Result of a fetch operation
|
|
741
|
+
*/
|
|
742
|
+
interface FetchResult {
|
|
743
|
+
/** The fetched GeoJSON data */
|
|
744
|
+
data: FeatureCollection;
|
|
745
|
+
/** Whether data came from cache */
|
|
746
|
+
fromCache: boolean;
|
|
747
|
+
/** Number of features in the collection */
|
|
748
|
+
featureCount: number;
|
|
749
|
+
/** Duration of fetch operation in milliseconds */
|
|
750
|
+
duration: number;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* HTTP data fetcher with caching and retry support.
|
|
754
|
+
*
|
|
755
|
+
* @remarks
|
|
756
|
+
* Features:
|
|
757
|
+
* - In-memory caching with TTL
|
|
758
|
+
* - Conditional requests (If-None-Match, If-Modified-Since)
|
|
759
|
+
* - Automatic retry with exponential backoff
|
|
760
|
+
* - Request timeout and cancellation
|
|
761
|
+
* - GeoJSON validation
|
|
762
|
+
* - Lifecycle callbacks
|
|
763
|
+
*
|
|
764
|
+
* @example
|
|
765
|
+
* ```typescript
|
|
766
|
+
* const fetcher = new DataFetcher({
|
|
767
|
+
* cache: { enabled: true, defaultTTL: 300000, maxSize: 100 },
|
|
768
|
+
* retry: { enabled: true, maxRetries: 3, initialDelay: 1000, maxDelay: 10000 },
|
|
769
|
+
* timeout: 30000,
|
|
770
|
+
* });
|
|
771
|
+
*
|
|
772
|
+
* // Fetch with caching
|
|
773
|
+
* const result = await fetcher.fetch('https://example.com/data.geojson');
|
|
774
|
+
* console.log(`Fetched ${result.featureCount} features in ${result.duration}ms`);
|
|
775
|
+
*
|
|
776
|
+
* // Prefetch for later use
|
|
777
|
+
* await fetcher.prefetch('https://example.com/data2.geojson');
|
|
778
|
+
*
|
|
779
|
+
* // Force fresh fetch
|
|
780
|
+
* const fresh = await fetcher.fetch(url, { skipCache: true });
|
|
781
|
+
* ```
|
|
782
|
+
*/
|
|
783
|
+
declare class DataFetcher {
|
|
784
|
+
private static readonly DEFAULT_CONFIG;
|
|
785
|
+
private config;
|
|
786
|
+
private cache;
|
|
787
|
+
private retryManager;
|
|
788
|
+
private activeRequests;
|
|
789
|
+
/**
|
|
790
|
+
* Create a new DataFetcher instance
|
|
791
|
+
*
|
|
792
|
+
* @param config - Fetcher configuration
|
|
793
|
+
*/
|
|
794
|
+
constructor(config?: Partial<FetcherConfig>);
|
|
795
|
+
/**
|
|
796
|
+
* Fetch GeoJSON data from a URL
|
|
797
|
+
*
|
|
798
|
+
* @param url - URL to fetch from
|
|
799
|
+
* @param options - Fetch options
|
|
800
|
+
* @returns Fetch result with data and metadata
|
|
801
|
+
* @throws {Error} On network error, timeout, invalid JSON, or non-GeoJSON response
|
|
802
|
+
*
|
|
803
|
+
* @example
|
|
804
|
+
* ```typescript
|
|
805
|
+
* const result = await fetcher.fetch(
|
|
806
|
+
* 'https://example.com/data.geojson',
|
|
807
|
+
* {
|
|
808
|
+
* ttl: 60000, // 1 minute cache
|
|
809
|
+
* onRetry: (attempt, delay, error) => {
|
|
810
|
+
* console.log(`Retry ${attempt} in ${delay}ms: ${error.message}`);
|
|
811
|
+
* },
|
|
812
|
+
* }
|
|
813
|
+
* );
|
|
814
|
+
* ```
|
|
815
|
+
*/
|
|
816
|
+
fetch(url: string, options?: FetchOptions): Promise<FetchResult>;
|
|
817
|
+
/**
|
|
818
|
+
* Prefetch data and store in cache
|
|
819
|
+
*
|
|
820
|
+
* @remarks
|
|
821
|
+
* Useful for preloading data that will be needed soon.
|
|
822
|
+
* Does not return the data.
|
|
823
|
+
*
|
|
824
|
+
* @param url - URL to prefetch
|
|
825
|
+
* @param ttl - Optional custom TTL for cached entry
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* ```typescript
|
|
829
|
+
* // Prefetch data for quick access later
|
|
830
|
+
* await fetcher.prefetch('https://example.com/data.geojson', 600000);
|
|
831
|
+
* ```
|
|
832
|
+
*/
|
|
833
|
+
prefetch(url: string, ttl?: number): Promise<void>;
|
|
834
|
+
/**
|
|
835
|
+
* Invalidate cached entry for a URL
|
|
836
|
+
*
|
|
837
|
+
* @param url - URL to invalidate
|
|
838
|
+
*
|
|
839
|
+
* @example
|
|
840
|
+
* ```typescript
|
|
841
|
+
* // Force next fetch to get fresh data
|
|
842
|
+
* fetcher.invalidate('https://example.com/data.geojson');
|
|
843
|
+
* ```
|
|
844
|
+
*/
|
|
845
|
+
invalidate(url: string): void;
|
|
846
|
+
/**
|
|
847
|
+
* Clear all cached entries
|
|
848
|
+
*/
|
|
849
|
+
clearCache(): void;
|
|
850
|
+
/**
|
|
851
|
+
* Get cache statistics
|
|
852
|
+
*
|
|
853
|
+
* @returns Cache stats including size, hits, misses, and hit rate
|
|
854
|
+
*/
|
|
855
|
+
getCacheStats(): CacheStats;
|
|
856
|
+
/**
|
|
857
|
+
* Abort all active requests
|
|
858
|
+
*/
|
|
859
|
+
abortAll(): void;
|
|
860
|
+
/**
|
|
861
|
+
* Fetch with retry logic
|
|
862
|
+
*/
|
|
863
|
+
private fetchWithRetry;
|
|
864
|
+
/**
|
|
865
|
+
* Perform the actual HTTP fetch
|
|
866
|
+
*/
|
|
867
|
+
private performFetch;
|
|
868
|
+
/**
|
|
869
|
+
* Check if an error should trigger a retry
|
|
870
|
+
*/
|
|
871
|
+
private isRetryableError;
|
|
872
|
+
/**
|
|
873
|
+
* Validate that data is a GeoJSON FeatureCollection
|
|
874
|
+
*/
|
|
875
|
+
private isValidGeoJSON;
|
|
876
|
+
/**
|
|
877
|
+
* Merge partial config with defaults
|
|
878
|
+
*/
|
|
879
|
+
private mergeConfig;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* @file Exponential backoff retry manager
|
|
884
|
+
* @module @maplibre-yaml/core/data
|
|
885
|
+
*/
|
|
886
|
+
/**
|
|
887
|
+
* Configuration options for retry behavior
|
|
888
|
+
*/
|
|
889
|
+
interface RetryConfig {
|
|
890
|
+
/**
|
|
891
|
+
* Maximum number of retry attempts
|
|
892
|
+
* @default 10
|
|
893
|
+
*/
|
|
894
|
+
maxRetries: number;
|
|
895
|
+
/**
|
|
896
|
+
* Initial delay in milliseconds before first retry
|
|
897
|
+
* @default 1000
|
|
898
|
+
*/
|
|
899
|
+
initialDelay: number;
|
|
900
|
+
/**
|
|
901
|
+
* Maximum delay in milliseconds between retries
|
|
902
|
+
* @default 30000
|
|
903
|
+
*/
|
|
904
|
+
maxDelay: number;
|
|
905
|
+
/**
|
|
906
|
+
* Backoff multiplier for exponential backoff
|
|
907
|
+
* @default 2
|
|
908
|
+
*/
|
|
909
|
+
backoffFactor: number;
|
|
910
|
+
/**
|
|
911
|
+
* Whether to apply random jitter to delays
|
|
912
|
+
* @default true
|
|
913
|
+
*/
|
|
914
|
+
jitter: boolean;
|
|
915
|
+
/**
|
|
916
|
+
* Jitter factor (percentage of delay to randomize)
|
|
917
|
+
* @default 0.25
|
|
918
|
+
*/
|
|
919
|
+
jitterFactor: number;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Callbacks for retry lifecycle events
|
|
923
|
+
*/
|
|
924
|
+
interface RetryCallbacks {
|
|
925
|
+
/**
|
|
926
|
+
* Called before each retry attempt
|
|
927
|
+
*
|
|
928
|
+
* @param attempt - Current attempt number (1-indexed)
|
|
929
|
+
* @param delay - Delay in milliseconds before this retry
|
|
930
|
+
* @param error - Error that triggered the retry
|
|
931
|
+
*/
|
|
932
|
+
onRetry?: (attempt: number, delay: number, error: Error) => void;
|
|
933
|
+
/**
|
|
934
|
+
* Called when all retry attempts are exhausted
|
|
935
|
+
*
|
|
936
|
+
* @param attempts - Total number of attempts made
|
|
937
|
+
* @param lastError - The final error
|
|
938
|
+
*/
|
|
939
|
+
onExhausted?: (attempts: number, lastError: Error) => void;
|
|
940
|
+
/**
|
|
941
|
+
* Called when operation succeeds
|
|
942
|
+
*
|
|
943
|
+
* @param attempts - Number of attempts before success (1 = first try)
|
|
944
|
+
*/
|
|
945
|
+
onSuccess?: (attempts: number) => void;
|
|
946
|
+
/**
|
|
947
|
+
* Predicate to determine if an error is retryable
|
|
948
|
+
*
|
|
949
|
+
* @param error - Error to check
|
|
950
|
+
* @returns True if the error should trigger a retry
|
|
951
|
+
*
|
|
952
|
+
* @default All errors are retryable
|
|
953
|
+
*/
|
|
954
|
+
isRetryable?: (error: Error) => boolean;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Error thrown when maximum retry attempts are exceeded
|
|
958
|
+
*/
|
|
959
|
+
declare class MaxRetriesExceededError extends Error {
|
|
960
|
+
lastError: Error;
|
|
961
|
+
attempts: number;
|
|
962
|
+
/**
|
|
963
|
+
* Create a MaxRetriesExceededError
|
|
964
|
+
*
|
|
965
|
+
* @param lastError - The error from the final attempt
|
|
966
|
+
* @param attempts - Number of attempts made
|
|
967
|
+
*/
|
|
968
|
+
constructor(lastError: Error, attempts: number);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Retry manager with exponential backoff and jitter.
|
|
972
|
+
*
|
|
973
|
+
* @remarks
|
|
974
|
+
* Implements exponential backoff with the formula:
|
|
975
|
+
* ```
|
|
976
|
+
* delay = min(initialDelay * (backoffFactor ^ (attempt - 1)), maxDelay)
|
|
977
|
+
* ```
|
|
978
|
+
*
|
|
979
|
+
* With jitter applied as:
|
|
980
|
+
* ```
|
|
981
|
+
* delay = delay + (random(-1, 1) * delay * jitterFactor)
|
|
982
|
+
* ```
|
|
983
|
+
*
|
|
984
|
+
* Jitter helps prevent thundering herd problems when multiple clients
|
|
985
|
+
* retry simultaneously.
|
|
986
|
+
*
|
|
987
|
+
* @example
|
|
988
|
+
* ```typescript
|
|
989
|
+
* const retry = new RetryManager({
|
|
990
|
+
* maxRetries: 5,
|
|
991
|
+
* initialDelay: 1000,
|
|
992
|
+
* backoffFactor: 2,
|
|
993
|
+
* });
|
|
994
|
+
*
|
|
995
|
+
* try {
|
|
996
|
+
* const result = await retry.execute(
|
|
997
|
+
* async () => {
|
|
998
|
+
* const response = await fetch('https://api.example.com/data');
|
|
999
|
+
* if (!response.ok) throw new Error('Request failed');
|
|
1000
|
+
* return response.json();
|
|
1001
|
+
* },
|
|
1002
|
+
* {
|
|
1003
|
+
* onRetry: (attempt, delay, error) => {
|
|
1004
|
+
* console.log(`Retry ${attempt} in ${delay}ms: ${error.message}`);
|
|
1005
|
+
* },
|
|
1006
|
+
* }
|
|
1007
|
+
* );
|
|
1008
|
+
* console.log('Success:', result);
|
|
1009
|
+
* } catch (error) {
|
|
1010
|
+
* console.error('All retries failed:', error);
|
|
1011
|
+
* }
|
|
1012
|
+
* ```
|
|
1013
|
+
*/
|
|
1014
|
+
declare class RetryManager {
|
|
1015
|
+
private static readonly DEFAULT_CONFIG;
|
|
1016
|
+
private config;
|
|
1017
|
+
/**
|
|
1018
|
+
* Create a new RetryManager instance
|
|
1019
|
+
*
|
|
1020
|
+
* @param config - Retry configuration options
|
|
1021
|
+
*/
|
|
1022
|
+
constructor(config?: Partial<RetryConfig>);
|
|
1023
|
+
/**
|
|
1024
|
+
* Execute a function with retry logic
|
|
1025
|
+
*
|
|
1026
|
+
* @typeParam T - Return type of the function
|
|
1027
|
+
* @param fn - Async function to execute with retries
|
|
1028
|
+
* @param callbacks - Optional lifecycle callbacks
|
|
1029
|
+
* @returns Promise that resolves with the function's result
|
|
1030
|
+
* @throws {MaxRetriesExceededError} When all retry attempts fail
|
|
1031
|
+
*
|
|
1032
|
+
* @example
|
|
1033
|
+
* ```typescript
|
|
1034
|
+
* const data = await retry.execute(
|
|
1035
|
+
* () => fetchData(url),
|
|
1036
|
+
* {
|
|
1037
|
+
* isRetryable: (error) => {
|
|
1038
|
+
* // Don't retry 4xx errors except 429 (rate limit)
|
|
1039
|
+
* if (error.message.includes('429')) return true;
|
|
1040
|
+
* if (error.message.match(/4\d\d/)) return false;
|
|
1041
|
+
* return true;
|
|
1042
|
+
* },
|
|
1043
|
+
* }
|
|
1044
|
+
* );
|
|
1045
|
+
* ```
|
|
1046
|
+
*/
|
|
1047
|
+
execute<T>(fn: () => Promise<T>, callbacks?: RetryCallbacks): Promise<T>;
|
|
1048
|
+
/**
|
|
1049
|
+
* Calculate delay for a given attempt using exponential backoff
|
|
1050
|
+
*
|
|
1051
|
+
* @param attempt - Current attempt number (1-indexed)
|
|
1052
|
+
* @returns Delay in milliseconds
|
|
1053
|
+
*
|
|
1054
|
+
* @example
|
|
1055
|
+
* ```typescript
|
|
1056
|
+
* const retry = new RetryManager({ initialDelay: 1000, backoffFactor: 2 });
|
|
1057
|
+
* console.log(retry.calculateDelay(1)); // ~1000ms
|
|
1058
|
+
* console.log(retry.calculateDelay(2)); // ~2000ms
|
|
1059
|
+
* console.log(retry.calculateDelay(3)); // ~4000ms
|
|
1060
|
+
* ```
|
|
1061
|
+
*/
|
|
1062
|
+
calculateDelay(attempt: number): number;
|
|
1063
|
+
/**
|
|
1064
|
+
* Reset internal state
|
|
1065
|
+
*
|
|
1066
|
+
* @remarks
|
|
1067
|
+
* Currently this class is stateless, but this method is provided
|
|
1068
|
+
* for API consistency and future extensibility.
|
|
1069
|
+
*/
|
|
1070
|
+
reset(): void;
|
|
1071
|
+
/**
|
|
1072
|
+
* Sleep for specified milliseconds
|
|
1073
|
+
*
|
|
1074
|
+
* @param ms - Milliseconds to sleep
|
|
1075
|
+
* @returns Promise that resolves after the delay
|
|
1076
|
+
*/
|
|
1077
|
+
private sleep;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* @file Polling manager for periodic data refresh
|
|
1082
|
+
* @module @maplibre-yaml/core/data
|
|
1083
|
+
*/
|
|
1084
|
+
/**
|
|
1085
|
+
* Configuration for a polling subscription.
|
|
1086
|
+
*
|
|
1087
|
+
* @remarks
|
|
1088
|
+
* The polling manager ensures non-overlapping execution by waiting for each
|
|
1089
|
+
* tick to complete before scheduling the next one. This prevents concurrent
|
|
1090
|
+
* execution of the same polling task.
|
|
1091
|
+
*
|
|
1092
|
+
* @example
|
|
1093
|
+
* ```typescript
|
|
1094
|
+
* const polling = new PollingManager();
|
|
1095
|
+
*
|
|
1096
|
+
* polling.start('vehicles', {
|
|
1097
|
+
* interval: 5000,
|
|
1098
|
+
* onTick: async () => {
|
|
1099
|
+
* const data = await fetch('/api/vehicles');
|
|
1100
|
+
* updateMap(data);
|
|
1101
|
+
* },
|
|
1102
|
+
* onError: (error) => console.error(error),
|
|
1103
|
+
* immediate: true,
|
|
1104
|
+
* pauseWhenHidden: true,
|
|
1105
|
+
* });
|
|
1106
|
+
* ```
|
|
1107
|
+
*/
|
|
1108
|
+
interface PollingConfig {
|
|
1109
|
+
/** Polling interval in milliseconds (minimum 1000ms) */
|
|
1110
|
+
interval: number;
|
|
1111
|
+
/** Function to execute on each tick */
|
|
1112
|
+
onTick: () => Promise<void>;
|
|
1113
|
+
/** Error handler for tick failures */
|
|
1114
|
+
onError?: (error: Error) => void;
|
|
1115
|
+
/** Execute immediately on start (default: false) */
|
|
1116
|
+
immediate?: boolean;
|
|
1117
|
+
/** Pause polling when document is hidden (default: true) */
|
|
1118
|
+
pauseWhenHidden?: boolean;
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Current state of a polling subscription.
|
|
1122
|
+
*/
|
|
1123
|
+
interface PollingState {
|
|
1124
|
+
/** Whether polling is active */
|
|
1125
|
+
isActive: boolean;
|
|
1126
|
+
/** Whether polling is paused */
|
|
1127
|
+
isPaused: boolean;
|
|
1128
|
+
/** Timestamp of last successful tick */
|
|
1129
|
+
lastTick: number | null;
|
|
1130
|
+
/** Timestamp of next scheduled tick */
|
|
1131
|
+
nextTick: number | null;
|
|
1132
|
+
/** Total number of successful ticks */
|
|
1133
|
+
tickCount: number;
|
|
1134
|
+
/** Total number of errors */
|
|
1135
|
+
errorCount: number;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Manages polling intervals for data refresh.
|
|
1139
|
+
*
|
|
1140
|
+
* @remarks
|
|
1141
|
+
* Features:
|
|
1142
|
+
* - Independent intervals per subscription
|
|
1143
|
+
* - Visibility-aware (pause when tab hidden)
|
|
1144
|
+
* - Tracks tick count and error count
|
|
1145
|
+
* - Non-overlapping execution (waits for tick to complete)
|
|
1146
|
+
* - Pause/resume functionality
|
|
1147
|
+
* - Manual trigger support
|
|
1148
|
+
*
|
|
1149
|
+
* The polling manager automatically pauses polling when the document becomes
|
|
1150
|
+
* hidden (unless `pauseWhenHidden` is false) and resumes when visible again.
|
|
1151
|
+
*
|
|
1152
|
+
* @example
|
|
1153
|
+
* ```typescript
|
|
1154
|
+
* const polling = new PollingManager();
|
|
1155
|
+
*
|
|
1156
|
+
* // Start polling
|
|
1157
|
+
* polling.start('data-refresh', {
|
|
1158
|
+
* interval: 10000,
|
|
1159
|
+
* onTick: async () => {
|
|
1160
|
+
* await fetchAndUpdateData();
|
|
1161
|
+
* },
|
|
1162
|
+
* immediate: true,
|
|
1163
|
+
* });
|
|
1164
|
+
*
|
|
1165
|
+
* // Pause temporarily
|
|
1166
|
+
* polling.pause('data-refresh');
|
|
1167
|
+
*
|
|
1168
|
+
* // Resume
|
|
1169
|
+
* polling.resume('data-refresh');
|
|
1170
|
+
*
|
|
1171
|
+
* // Trigger immediately
|
|
1172
|
+
* await polling.triggerNow('data-refresh');
|
|
1173
|
+
*
|
|
1174
|
+
* // Stop completely
|
|
1175
|
+
* polling.stop('data-refresh');
|
|
1176
|
+
* ```
|
|
1177
|
+
*/
|
|
1178
|
+
declare class PollingManager {
|
|
1179
|
+
private subscriptions;
|
|
1180
|
+
private visibilityListener;
|
|
1181
|
+
constructor();
|
|
1182
|
+
/**
|
|
1183
|
+
* Start a new polling subscription.
|
|
1184
|
+
*
|
|
1185
|
+
* @param id - Unique identifier for the subscription
|
|
1186
|
+
* @param config - Polling configuration
|
|
1187
|
+
* @throws Error if a subscription with the same ID already exists
|
|
1188
|
+
*
|
|
1189
|
+
* @example
|
|
1190
|
+
* ```typescript
|
|
1191
|
+
* polling.start('layer-1', {
|
|
1192
|
+
* interval: 5000,
|
|
1193
|
+
* onTick: async () => {
|
|
1194
|
+
* await updateLayerData();
|
|
1195
|
+
* },
|
|
1196
|
+
* });
|
|
1197
|
+
* ```
|
|
1198
|
+
*/
|
|
1199
|
+
start(id: string, config: PollingConfig): void;
|
|
1200
|
+
/**
|
|
1201
|
+
* Stop a polling subscription and clean up resources.
|
|
1202
|
+
*
|
|
1203
|
+
* @param id - Subscription identifier
|
|
1204
|
+
*
|
|
1205
|
+
* @example
|
|
1206
|
+
* ```typescript
|
|
1207
|
+
* polling.stop('layer-1');
|
|
1208
|
+
* ```
|
|
1209
|
+
*/
|
|
1210
|
+
stop(id: string): void;
|
|
1211
|
+
/**
|
|
1212
|
+
* Stop all polling subscriptions.
|
|
1213
|
+
*
|
|
1214
|
+
* @example
|
|
1215
|
+
* ```typescript
|
|
1216
|
+
* polling.stopAll();
|
|
1217
|
+
* ```
|
|
1218
|
+
*/
|
|
1219
|
+
stopAll(): void;
|
|
1220
|
+
/**
|
|
1221
|
+
* Pause a polling subscription without stopping it.
|
|
1222
|
+
*
|
|
1223
|
+
* @param id - Subscription identifier
|
|
1224
|
+
*
|
|
1225
|
+
* @remarks
|
|
1226
|
+
* Paused subscriptions can be resumed with {@link resume}.
|
|
1227
|
+
* The subscription maintains its state while paused.
|
|
1228
|
+
*
|
|
1229
|
+
* @example
|
|
1230
|
+
* ```typescript
|
|
1231
|
+
* polling.pause('layer-1');
|
|
1232
|
+
* ```
|
|
1233
|
+
*/
|
|
1234
|
+
pause(id: string): void;
|
|
1235
|
+
/**
|
|
1236
|
+
* Pause all active polling subscriptions.
|
|
1237
|
+
*
|
|
1238
|
+
* @example
|
|
1239
|
+
* ```typescript
|
|
1240
|
+
* polling.pauseAll();
|
|
1241
|
+
* ```
|
|
1242
|
+
*/
|
|
1243
|
+
pauseAll(): void;
|
|
1244
|
+
/**
|
|
1245
|
+
* Resume a paused polling subscription.
|
|
1246
|
+
*
|
|
1247
|
+
* @param id - Subscription identifier
|
|
1248
|
+
*
|
|
1249
|
+
* @example
|
|
1250
|
+
* ```typescript
|
|
1251
|
+
* polling.resume('layer-1');
|
|
1252
|
+
* ```
|
|
1253
|
+
*/
|
|
1254
|
+
resume(id: string): void;
|
|
1255
|
+
/**
|
|
1256
|
+
* Resume all paused polling subscriptions.
|
|
1257
|
+
*
|
|
1258
|
+
* @example
|
|
1259
|
+
* ```typescript
|
|
1260
|
+
* polling.resumeAll();
|
|
1261
|
+
* ```
|
|
1262
|
+
*/
|
|
1263
|
+
resumeAll(): void;
|
|
1264
|
+
/**
|
|
1265
|
+
* Trigger an immediate execution of the polling tick.
|
|
1266
|
+
*
|
|
1267
|
+
* @param id - Subscription identifier
|
|
1268
|
+
* @returns Promise that resolves when the tick completes
|
|
1269
|
+
* @throws Error if the subscription doesn't exist
|
|
1270
|
+
*
|
|
1271
|
+
* @remarks
|
|
1272
|
+
* This does not affect the regular polling schedule. The next scheduled
|
|
1273
|
+
* tick will still occur at the expected time.
|
|
1274
|
+
*
|
|
1275
|
+
* @example
|
|
1276
|
+
* ```typescript
|
|
1277
|
+
* await polling.triggerNow('layer-1');
|
|
1278
|
+
* ```
|
|
1279
|
+
*/
|
|
1280
|
+
triggerNow(id: string): Promise<void>;
|
|
1281
|
+
/**
|
|
1282
|
+
* Get the current state of a polling subscription.
|
|
1283
|
+
*
|
|
1284
|
+
* @param id - Subscription identifier
|
|
1285
|
+
* @returns Current state or null if not found
|
|
1286
|
+
*
|
|
1287
|
+
* @example
|
|
1288
|
+
* ```typescript
|
|
1289
|
+
* const state = polling.getState('layer-1');
|
|
1290
|
+
* if (state) {
|
|
1291
|
+
* console.log(`Ticks: ${state.tickCount}, Errors: ${state.errorCount}`);
|
|
1292
|
+
* }
|
|
1293
|
+
* ```
|
|
1294
|
+
*/
|
|
1295
|
+
getState(id: string): PollingState | null;
|
|
1296
|
+
/**
|
|
1297
|
+
* Get all active polling subscription IDs.
|
|
1298
|
+
*
|
|
1299
|
+
* @returns Array of subscription IDs
|
|
1300
|
+
*
|
|
1301
|
+
* @example
|
|
1302
|
+
* ```typescript
|
|
1303
|
+
* const ids = polling.getActiveIds();
|
|
1304
|
+
* console.log(`Active pollers: ${ids.join(', ')}`);
|
|
1305
|
+
* ```
|
|
1306
|
+
*/
|
|
1307
|
+
getActiveIds(): string[];
|
|
1308
|
+
/**
|
|
1309
|
+
* Check if a polling subscription exists.
|
|
1310
|
+
*
|
|
1311
|
+
* @param id - Subscription identifier
|
|
1312
|
+
* @returns True if the subscription exists
|
|
1313
|
+
*
|
|
1314
|
+
* @example
|
|
1315
|
+
* ```typescript
|
|
1316
|
+
* if (polling.has('layer-1')) {
|
|
1317
|
+
* polling.pause('layer-1');
|
|
1318
|
+
* }
|
|
1319
|
+
* ```
|
|
1320
|
+
*/
|
|
1321
|
+
has(id: string): boolean;
|
|
1322
|
+
/**
|
|
1323
|
+
* Update the interval for an active polling subscription.
|
|
1324
|
+
*
|
|
1325
|
+
* @param id - Subscription identifier
|
|
1326
|
+
* @param interval - New interval in milliseconds (minimum 1000ms)
|
|
1327
|
+
* @throws Error if the subscription doesn't exist
|
|
1328
|
+
*
|
|
1329
|
+
* @remarks
|
|
1330
|
+
* The new interval takes effect after the current tick completes.
|
|
1331
|
+
*
|
|
1332
|
+
* @example
|
|
1333
|
+
* ```typescript
|
|
1334
|
+
* polling.setInterval('layer-1', 10000);
|
|
1335
|
+
* ```
|
|
1336
|
+
*/
|
|
1337
|
+
setInterval(id: string, interval: number): void;
|
|
1338
|
+
/**
|
|
1339
|
+
* Clean up all resources and stop all polling.
|
|
1340
|
+
*
|
|
1341
|
+
* @remarks
|
|
1342
|
+
* Should be called when the polling manager is no longer needed.
|
|
1343
|
+
* After calling destroy, the polling manager should not be used.
|
|
1344
|
+
*
|
|
1345
|
+
* @example
|
|
1346
|
+
* ```typescript
|
|
1347
|
+
* polling.destroy();
|
|
1348
|
+
* ```
|
|
1349
|
+
*/
|
|
1350
|
+
destroy(): void;
|
|
1351
|
+
/**
|
|
1352
|
+
* Execute a single tick for a subscription.
|
|
1353
|
+
*/
|
|
1354
|
+
private executeTick;
|
|
1355
|
+
/**
|
|
1356
|
+
* Schedule the next tick for a subscription.
|
|
1357
|
+
*/
|
|
1358
|
+
private scheduleNextTick;
|
|
1359
|
+
/**
|
|
1360
|
+
* Setup document visibility listener for automatic pause/resume.
|
|
1361
|
+
*/
|
|
1362
|
+
private setupVisibilityListener;
|
|
1363
|
+
/**
|
|
1364
|
+
* Handle document visibility changes.
|
|
1365
|
+
*/
|
|
1366
|
+
private handleVisibilityChange;
|
|
1367
|
+
/**
|
|
1368
|
+
* Remove document visibility listener.
|
|
1369
|
+
*/
|
|
1370
|
+
private teardownVisibilityListener;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* @file Type-safe event emitter utility
|
|
1375
|
+
* @module @maplibre-yaml/core/utils
|
|
1376
|
+
*/
|
|
1377
|
+
/**
|
|
1378
|
+
* Event handler function type
|
|
1379
|
+
*/
|
|
1380
|
+
type EventHandler<T> = (data: T) => void;
|
|
1381
|
+
/**
|
|
1382
|
+
* Type-safe event emitter with strongly-typed event names and payloads
|
|
1383
|
+
*
|
|
1384
|
+
* @remarks
|
|
1385
|
+
* This class provides a type-safe event emitter pattern where event names
|
|
1386
|
+
* and their payload types are strictly enforced. It's designed to be extended
|
|
1387
|
+
* by classes that need event emission capabilities.
|
|
1388
|
+
*
|
|
1389
|
+
* @example
|
|
1390
|
+
* ```typescript
|
|
1391
|
+
* interface MyEvents {
|
|
1392
|
+
* connect: void;
|
|
1393
|
+
* message: { text: string };
|
|
1394
|
+
* error: { error: Error };
|
|
1395
|
+
* }
|
|
1396
|
+
*
|
|
1397
|
+
* class MyEmitter extends EventEmitter<MyEvents> {
|
|
1398
|
+
* connect() {
|
|
1399
|
+
* this.emit('connect', undefined);
|
|
1400
|
+
* }
|
|
1401
|
+
*
|
|
1402
|
+
* sendMessage(text: string) {
|
|
1403
|
+
* this.emit('message', { text });
|
|
1404
|
+
* }
|
|
1405
|
+
* }
|
|
1406
|
+
*
|
|
1407
|
+
* const emitter = new MyEmitter();
|
|
1408
|
+
* emitter.on('message', (data) => {
|
|
1409
|
+
* console.log(data.text); // Strongly typed!
|
|
1410
|
+
* });
|
|
1411
|
+
* ```
|
|
1412
|
+
*
|
|
1413
|
+
* @typeParam Events - Record of event names to payload types
|
|
1414
|
+
*/
|
|
1415
|
+
declare class EventEmitter<Events extends Record<string, unknown>> {
|
|
1416
|
+
private handlers;
|
|
1417
|
+
/**
|
|
1418
|
+
* Register an event handler
|
|
1419
|
+
*
|
|
1420
|
+
* @param event - Event name to listen for
|
|
1421
|
+
* @param handler - Callback function to invoke when event is emitted
|
|
1422
|
+
* @returns Unsubscribe function that removes this specific handler
|
|
1423
|
+
*
|
|
1424
|
+
* @example
|
|
1425
|
+
* ```typescript
|
|
1426
|
+
* const unsubscribe = emitter.on('message', (data) => {
|
|
1427
|
+
* console.log(data.text);
|
|
1428
|
+
* });
|
|
1429
|
+
*
|
|
1430
|
+
* // Later, to unsubscribe:
|
|
1431
|
+
* unsubscribe();
|
|
1432
|
+
* ```
|
|
1433
|
+
*/
|
|
1434
|
+
on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): () => void;
|
|
1435
|
+
/**
|
|
1436
|
+
* Register a one-time event handler
|
|
1437
|
+
*
|
|
1438
|
+
* @remarks
|
|
1439
|
+
* The handler will be automatically removed after being invoked once.
|
|
1440
|
+
*
|
|
1441
|
+
* @param event - Event name to listen for
|
|
1442
|
+
* @param handler - Callback function to invoke once
|
|
1443
|
+
*
|
|
1444
|
+
* @example
|
|
1445
|
+
* ```typescript
|
|
1446
|
+
* emitter.once('connect', () => {
|
|
1447
|
+
* console.log('Connected!');
|
|
1448
|
+
* });
|
|
1449
|
+
* ```
|
|
1450
|
+
*/
|
|
1451
|
+
once<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void;
|
|
1452
|
+
/**
|
|
1453
|
+
* Remove an event handler
|
|
1454
|
+
*
|
|
1455
|
+
* @param event - Event name
|
|
1456
|
+
* @param handler - Handler function to remove
|
|
1457
|
+
*
|
|
1458
|
+
* @example
|
|
1459
|
+
* ```typescript
|
|
1460
|
+
* const handler = (data) => console.log(data);
|
|
1461
|
+
* emitter.on('message', handler);
|
|
1462
|
+
* emitter.off('message', handler);
|
|
1463
|
+
* ```
|
|
1464
|
+
*/
|
|
1465
|
+
off<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void;
|
|
1466
|
+
/**
|
|
1467
|
+
* Emit an event to all registered handlers
|
|
1468
|
+
*
|
|
1469
|
+
* @remarks
|
|
1470
|
+
* This method is protected to ensure only the extending class can emit events.
|
|
1471
|
+
* All handlers are invoked synchronously in the order they were registered.
|
|
1472
|
+
*
|
|
1473
|
+
* @param event - Event name to emit
|
|
1474
|
+
* @param data - Event payload data
|
|
1475
|
+
*
|
|
1476
|
+
* @example
|
|
1477
|
+
* ```typescript
|
|
1478
|
+
* class MyEmitter extends EventEmitter<MyEvents> {
|
|
1479
|
+
* doSomething() {
|
|
1480
|
+
* this.emit('something-happened', { value: 42 });
|
|
1481
|
+
* }
|
|
1482
|
+
* }
|
|
1483
|
+
* ```
|
|
1484
|
+
*/
|
|
1485
|
+
protected emit<K extends keyof Events>(event: K, data: Events[K]): void;
|
|
1486
|
+
/**
|
|
1487
|
+
* Remove all handlers for an event, or all handlers for all events
|
|
1488
|
+
*
|
|
1489
|
+
* @param event - Optional event name. If omitted, removes all handlers for all events.
|
|
1490
|
+
*
|
|
1491
|
+
* @example
|
|
1492
|
+
* ```typescript
|
|
1493
|
+
* // Remove all handlers for 'message' event
|
|
1494
|
+
* emitter.removeAllListeners('message');
|
|
1495
|
+
*
|
|
1496
|
+
* // Remove all handlers for all events
|
|
1497
|
+
* emitter.removeAllListeners();
|
|
1498
|
+
* ```
|
|
1499
|
+
*/
|
|
1500
|
+
removeAllListeners<K extends keyof Events>(event?: K): void;
|
|
1501
|
+
/**
|
|
1502
|
+
* Get the number of handlers registered for an event
|
|
1503
|
+
*
|
|
1504
|
+
* @param event - Event name
|
|
1505
|
+
* @returns Number of registered handlers
|
|
1506
|
+
*
|
|
1507
|
+
* @example
|
|
1508
|
+
* ```typescript
|
|
1509
|
+
* const count = emitter.listenerCount('message');
|
|
1510
|
+
* console.log(`${count} handlers registered`);
|
|
1511
|
+
* ```
|
|
1512
|
+
*/
|
|
1513
|
+
listenerCount<K extends keyof Events>(event: K): number;
|
|
1514
|
+
/**
|
|
1515
|
+
* Get all event names that have registered handlers
|
|
1516
|
+
*
|
|
1517
|
+
* @returns Array of event names
|
|
1518
|
+
*
|
|
1519
|
+
* @example
|
|
1520
|
+
* ```typescript
|
|
1521
|
+
* const events = emitter.eventNames();
|
|
1522
|
+
* console.log('Events with handlers:', events);
|
|
1523
|
+
* ```
|
|
1524
|
+
*/
|
|
1525
|
+
eventNames(): Array<keyof Events>;
|
|
1526
|
+
/**
|
|
1527
|
+
* Check if an event has any registered handlers
|
|
1528
|
+
*
|
|
1529
|
+
* @param event - Event name
|
|
1530
|
+
* @returns True if the event has at least one handler
|
|
1531
|
+
*
|
|
1532
|
+
* @example
|
|
1533
|
+
* ```typescript
|
|
1534
|
+
* if (emitter.hasListeners('message')) {
|
|
1535
|
+
* emitter.emit('message', { text: 'Hello' });
|
|
1536
|
+
* }
|
|
1537
|
+
* ```
|
|
1538
|
+
*/
|
|
1539
|
+
hasListeners<K extends keyof Events>(event: K): boolean;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* @file Base connection class for streaming connections
|
|
1544
|
+
* @module @maplibre-yaml/core/data/streaming
|
|
1545
|
+
*/
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Connection state enum.
|
|
1549
|
+
*
|
|
1550
|
+
* @remarks
|
|
1551
|
+
* State transitions:
|
|
1552
|
+
* - disconnected → connecting → connected
|
|
1553
|
+
* - connected → disconnected (on manual disconnect)
|
|
1554
|
+
* - connected → reconnecting → connected (on connection loss with reconnect enabled)
|
|
1555
|
+
* - reconnecting → failed (after max reconnect attempts)
|
|
1556
|
+
*/
|
|
1557
|
+
type ConnectionState = "disconnected" | "connecting" | "connected" | "reconnecting" | "failed";
|
|
1558
|
+
/**
|
|
1559
|
+
* Events emitted by streaming connections.
|
|
1560
|
+
*
|
|
1561
|
+
* @remarks
|
|
1562
|
+
* All connections emit these events to allow consumers to react to
|
|
1563
|
+
* connection lifecycle changes and incoming messages.
|
|
1564
|
+
*/
|
|
1565
|
+
interface ConnectionEvents extends Record<string, unknown> {
|
|
1566
|
+
/** Emitted when connection is established */
|
|
1567
|
+
connect: void;
|
|
1568
|
+
/** Emitted when connection is closed */
|
|
1569
|
+
disconnect: {
|
|
1570
|
+
reason: string;
|
|
1571
|
+
};
|
|
1572
|
+
/** Emitted when a message is received */
|
|
1573
|
+
message: {
|
|
1574
|
+
data: unknown;
|
|
1575
|
+
};
|
|
1576
|
+
/** Emitted when an error occurs */
|
|
1577
|
+
error: {
|
|
1578
|
+
error: Error;
|
|
1579
|
+
};
|
|
1580
|
+
/** Emitted when attempting to reconnect */
|
|
1581
|
+
reconnecting: {
|
|
1582
|
+
attempt: number;
|
|
1583
|
+
delay: number;
|
|
1584
|
+
};
|
|
1585
|
+
/** Emitted when reconnection succeeds */
|
|
1586
|
+
reconnected: {
|
|
1587
|
+
attempts: number;
|
|
1588
|
+
};
|
|
1589
|
+
/** Emitted when reconnection fails after max attempts */
|
|
1590
|
+
failed: {
|
|
1591
|
+
attempts: number;
|
|
1592
|
+
lastError: Error;
|
|
1593
|
+
};
|
|
1594
|
+
/** Emitted whenever connection state changes */
|
|
1595
|
+
stateChange: {
|
|
1596
|
+
from: ConnectionState;
|
|
1597
|
+
to: ConnectionState;
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Base configuration for all connection types.
|
|
1602
|
+
*/
|
|
1603
|
+
interface ConnectionConfig {
|
|
1604
|
+
/** Connection URL */
|
|
1605
|
+
url: string;
|
|
1606
|
+
/** Enable automatic reconnection on disconnect (default: true) */
|
|
1607
|
+
reconnect?: boolean;
|
|
1608
|
+
/** Reconnection retry configuration */
|
|
1609
|
+
reconnectConfig?: Partial<RetryConfig>;
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Abstract base class for streaming connections.
|
|
1613
|
+
*
|
|
1614
|
+
* @remarks
|
|
1615
|
+
* Provides common functionality for all streaming connection types:
|
|
1616
|
+
* - Connection state management
|
|
1617
|
+
* - Event emission via EventEmitter
|
|
1618
|
+
* - Automatic reconnection with exponential backoff
|
|
1619
|
+
* - State change tracking
|
|
1620
|
+
*
|
|
1621
|
+
* Subclasses must implement:
|
|
1622
|
+
* - `connect()`: Establish the connection
|
|
1623
|
+
* - `disconnect()`: Close the connection
|
|
1624
|
+
*
|
|
1625
|
+
* @example
|
|
1626
|
+
* ```typescript
|
|
1627
|
+
* class MyConnection extends BaseConnection {
|
|
1628
|
+
* async connect(): Promise<void> {
|
|
1629
|
+
* this.setState('connecting');
|
|
1630
|
+
* // ... connection logic
|
|
1631
|
+
* this.setState('connected');
|
|
1632
|
+
* this.emit('connect', undefined);
|
|
1633
|
+
* }
|
|
1634
|
+
*
|
|
1635
|
+
* disconnect(): void {
|
|
1636
|
+
* // ... disconnection logic
|
|
1637
|
+
* this.handleDisconnect('Manual disconnect');
|
|
1638
|
+
* }
|
|
1639
|
+
* }
|
|
1640
|
+
* ```
|
|
1641
|
+
*/
|
|
1642
|
+
declare abstract class BaseConnection extends EventEmitter<ConnectionEvents> {
|
|
1643
|
+
protected state: ConnectionState;
|
|
1644
|
+
protected config: Required<ConnectionConfig>;
|
|
1645
|
+
protected retryManager: RetryManager;
|
|
1646
|
+
protected reconnectAttempts: number;
|
|
1647
|
+
protected manualDisconnect: boolean;
|
|
1648
|
+
/**
|
|
1649
|
+
* Create a new base connection.
|
|
1650
|
+
*
|
|
1651
|
+
* @param config - Connection configuration
|
|
1652
|
+
*/
|
|
1653
|
+
constructor(config: ConnectionConfig);
|
|
1654
|
+
/**
|
|
1655
|
+
* Establish connection to the server.
|
|
1656
|
+
*
|
|
1657
|
+
* @remarks
|
|
1658
|
+
* Must be implemented by subclasses. Should:
|
|
1659
|
+
* 1. Set state to 'connecting'
|
|
1660
|
+
* 2. Establish the connection
|
|
1661
|
+
* 3. Set state to 'connected'
|
|
1662
|
+
* 4. Emit 'connect' event
|
|
1663
|
+
*
|
|
1664
|
+
* @throws Error if connection fails
|
|
1665
|
+
*/
|
|
1666
|
+
abstract connect(): Promise<void>;
|
|
1667
|
+
/**
|
|
1668
|
+
* Close the connection.
|
|
1669
|
+
*
|
|
1670
|
+
* @remarks
|
|
1671
|
+
* Must be implemented by subclasses. Should:
|
|
1672
|
+
* 1. Close the underlying connection
|
|
1673
|
+
* 2. Call handleDisconnect() with reason
|
|
1674
|
+
*/
|
|
1675
|
+
abstract disconnect(): void;
|
|
1676
|
+
/**
|
|
1677
|
+
* Get current connection state.
|
|
1678
|
+
*
|
|
1679
|
+
* @returns Current state
|
|
1680
|
+
*
|
|
1681
|
+
* @example
|
|
1682
|
+
* ```typescript
|
|
1683
|
+
* const state = connection.getState();
|
|
1684
|
+
* if (state === 'connected') {
|
|
1685
|
+
* // Connection is ready
|
|
1686
|
+
* }
|
|
1687
|
+
* ```
|
|
1688
|
+
*/
|
|
1689
|
+
getState(): ConnectionState;
|
|
1690
|
+
/**
|
|
1691
|
+
* Check if connection is currently connected.
|
|
1692
|
+
*
|
|
1693
|
+
* @returns True if connected
|
|
1694
|
+
*
|
|
1695
|
+
* @example
|
|
1696
|
+
* ```typescript
|
|
1697
|
+
* if (connection.isConnected()) {
|
|
1698
|
+
* connection.send(data);
|
|
1699
|
+
* }
|
|
1700
|
+
* ```
|
|
1701
|
+
*/
|
|
1702
|
+
isConnected(): boolean;
|
|
1703
|
+
/**
|
|
1704
|
+
* Get the number of reconnection attempts.
|
|
1705
|
+
*
|
|
1706
|
+
* @returns Number of reconnect attempts
|
|
1707
|
+
*/
|
|
1708
|
+
getReconnectAttempts(): number;
|
|
1709
|
+
/**
|
|
1710
|
+
* Update connection state and emit state change event.
|
|
1711
|
+
*
|
|
1712
|
+
* @param newState - New connection state
|
|
1713
|
+
*
|
|
1714
|
+
* @remarks
|
|
1715
|
+
* Automatically emits 'stateChange' event when state changes.
|
|
1716
|
+
* Subclasses should call this method instead of setting state directly.
|
|
1717
|
+
*
|
|
1718
|
+
* @example
|
|
1719
|
+
* ```typescript
|
|
1720
|
+
* protected async connect() {
|
|
1721
|
+
* this.setState('connecting');
|
|
1722
|
+
* await this.establishConnection();
|
|
1723
|
+
* this.setState('connected');
|
|
1724
|
+
* }
|
|
1725
|
+
* ```
|
|
1726
|
+
*/
|
|
1727
|
+
protected setState(newState: ConnectionState): void;
|
|
1728
|
+
/**
|
|
1729
|
+
* Handle disconnection and optionally attempt reconnection.
|
|
1730
|
+
*
|
|
1731
|
+
* @param reason - Reason for disconnection
|
|
1732
|
+
*
|
|
1733
|
+
* @remarks
|
|
1734
|
+
* This method should be called by subclasses when the connection is lost.
|
|
1735
|
+
* It will:
|
|
1736
|
+
* 1. Emit 'disconnect' event
|
|
1737
|
+
* 2. Attempt reconnection if enabled and not manually disconnected
|
|
1738
|
+
* 3. Emit 'reconnecting', 'reconnected', or 'failed' events as appropriate
|
|
1739
|
+
*
|
|
1740
|
+
* @example
|
|
1741
|
+
* ```typescript
|
|
1742
|
+
* ws.onclose = () => {
|
|
1743
|
+
* this.handleDisconnect('Connection closed');
|
|
1744
|
+
* };
|
|
1745
|
+
* ```
|
|
1746
|
+
*/
|
|
1747
|
+
protected handleDisconnect(reason: string): Promise<void>;
|
|
1748
|
+
/**
|
|
1749
|
+
* Attempt to reconnect with exponential backoff.
|
|
1750
|
+
*/
|
|
1751
|
+
private attemptReconnection;
|
|
1752
|
+
/**
|
|
1753
|
+
* Mark disconnection as manual to prevent reconnection.
|
|
1754
|
+
*
|
|
1755
|
+
* @remarks
|
|
1756
|
+
* Should be called by subclasses in their disconnect() implementation
|
|
1757
|
+
* before closing the connection.
|
|
1758
|
+
*
|
|
1759
|
+
* @example
|
|
1760
|
+
* ```typescript
|
|
1761
|
+
* disconnect(): void {
|
|
1762
|
+
* this.setManualDisconnect();
|
|
1763
|
+
* this.ws.close();
|
|
1764
|
+
* }
|
|
1765
|
+
* ```
|
|
1766
|
+
*/
|
|
1767
|
+
protected setManualDisconnect(): void;
|
|
1768
|
+
/**
|
|
1769
|
+
* Reset manual disconnect flag.
|
|
1770
|
+
*
|
|
1771
|
+
* @remarks
|
|
1772
|
+
* Should be called when establishing a new connection to allow
|
|
1773
|
+
* automatic reconnection for subsequent disconnections.
|
|
1774
|
+
*/
|
|
1775
|
+
protected resetManualDisconnect(): void;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
/**
|
|
1779
|
+
* @file Server-Sent Events connection implementation
|
|
1780
|
+
* @module @maplibre-yaml/core/data/streaming
|
|
1781
|
+
*/
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* Configuration for SSE connections.
|
|
1785
|
+
*
|
|
1786
|
+
* @remarks
|
|
1787
|
+
* Server-Sent Events (SSE) is a server push technology enabling a client
|
|
1788
|
+
* to receive automatic updates from a server via an HTTP connection.
|
|
1789
|
+
*
|
|
1790
|
+
* SSE is unidirectional (server to client only) and automatically handles
|
|
1791
|
+
* reconnection when the connection is lost.
|
|
1792
|
+
*/
|
|
1793
|
+
interface SSEConfig extends ConnectionConfig {
|
|
1794
|
+
/**
|
|
1795
|
+
* Event types to listen for (default: ['message'])
|
|
1796
|
+
*
|
|
1797
|
+
* @remarks
|
|
1798
|
+
* The EventSource API can listen for custom event types sent by the server.
|
|
1799
|
+
* By default, it listens to the 'message' event type.
|
|
1800
|
+
*
|
|
1801
|
+
* @example
|
|
1802
|
+
* ```typescript
|
|
1803
|
+
* eventTypes: ['update', 'delete', 'create']
|
|
1804
|
+
* ```
|
|
1805
|
+
*/
|
|
1806
|
+
eventTypes?: string[];
|
|
1807
|
+
/**
|
|
1808
|
+
* Include credentials in CORS requests (default: false)
|
|
1809
|
+
*
|
|
1810
|
+
* @remarks
|
|
1811
|
+
* When true, the EventSource will include credentials (cookies, authorization
|
|
1812
|
+
* headers, etc.) when making cross-origin requests.
|
|
1813
|
+
*/
|
|
1814
|
+
withCredentials?: boolean;
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Server-Sent Events connection.
|
|
1818
|
+
*
|
|
1819
|
+
* @remarks
|
|
1820
|
+
* Primary streaming mechanism for real-time updates. Uses the native
|
|
1821
|
+
* EventSource API for robust, automatic reconnection handling.
|
|
1822
|
+
*
|
|
1823
|
+
* Features:
|
|
1824
|
+
* - Automatic reconnection by the browser
|
|
1825
|
+
* - Event-based message streaming
|
|
1826
|
+
* - JSON message parsing with error handling
|
|
1827
|
+
* - Multiple event type support
|
|
1828
|
+
* - Last event ID tracking for resume
|
|
1829
|
+
*
|
|
1830
|
+
* @example
|
|
1831
|
+
* ```typescript
|
|
1832
|
+
* const sse = new SSEConnection({
|
|
1833
|
+
* url: 'https://api.example.com/events',
|
|
1834
|
+
* eventTypes: ['update', 'delete'],
|
|
1835
|
+
* });
|
|
1836
|
+
*
|
|
1837
|
+
* sse.on('message', ({ data }) => {
|
|
1838
|
+
* console.log('Received:', data);
|
|
1839
|
+
* });
|
|
1840
|
+
*
|
|
1841
|
+
* sse.on('error', ({ error }) => {
|
|
1842
|
+
* console.error('Error:', error);
|
|
1843
|
+
* });
|
|
1844
|
+
*
|
|
1845
|
+
* await sse.connect();
|
|
1846
|
+
* ```
|
|
1847
|
+
*/
|
|
1848
|
+
declare class SSEConnection extends BaseConnection {
|
|
1849
|
+
private eventSource;
|
|
1850
|
+
private lastEventId;
|
|
1851
|
+
private readonly sseConfig;
|
|
1852
|
+
/**
|
|
1853
|
+
* Create a new SSE connection.
|
|
1854
|
+
*
|
|
1855
|
+
* @param config - SSE configuration
|
|
1856
|
+
*
|
|
1857
|
+
* @example
|
|
1858
|
+
* ```typescript
|
|
1859
|
+
* const connection = new SSEConnection({
|
|
1860
|
+
* url: 'https://api.example.com/stream',
|
|
1861
|
+
* eventTypes: ['message', 'update'],
|
|
1862
|
+
* withCredentials: true,
|
|
1863
|
+
* });
|
|
1864
|
+
* ```
|
|
1865
|
+
*/
|
|
1866
|
+
constructor(config: SSEConfig);
|
|
1867
|
+
/**
|
|
1868
|
+
* Establish SSE connection.
|
|
1869
|
+
*
|
|
1870
|
+
* @remarks
|
|
1871
|
+
* Creates an EventSource and sets up event listeners for:
|
|
1872
|
+
* - Connection open
|
|
1873
|
+
* - Message events (for each configured event type)
|
|
1874
|
+
* - Error events
|
|
1875
|
+
*
|
|
1876
|
+
* The EventSource API handles reconnection automatically when the
|
|
1877
|
+
* connection is lost, unless explicitly closed.
|
|
1878
|
+
*
|
|
1879
|
+
* @throws Error if EventSource is not supported or connection fails
|
|
1880
|
+
*
|
|
1881
|
+
* @example
|
|
1882
|
+
* ```typescript
|
|
1883
|
+
* await connection.connect();
|
|
1884
|
+
* console.log('Connected to SSE stream');
|
|
1885
|
+
* ```
|
|
1886
|
+
*/
|
|
1887
|
+
connect(): Promise<void>;
|
|
1888
|
+
/**
|
|
1889
|
+
* Close SSE connection.
|
|
1890
|
+
*
|
|
1891
|
+
* @remarks
|
|
1892
|
+
* Closes the EventSource and cleans up all event listeners.
|
|
1893
|
+
* Sets the manual disconnect flag to prevent automatic reconnection.
|
|
1894
|
+
*
|
|
1895
|
+
* @example
|
|
1896
|
+
* ```typescript
|
|
1897
|
+
* connection.disconnect();
|
|
1898
|
+
* console.log('Disconnected from SSE stream');
|
|
1899
|
+
* ```
|
|
1900
|
+
*/
|
|
1901
|
+
disconnect(): void;
|
|
1902
|
+
/**
|
|
1903
|
+
* Get the last event ID received from the server.
|
|
1904
|
+
*
|
|
1905
|
+
* @returns Last event ID or null if none received
|
|
1906
|
+
*
|
|
1907
|
+
* @remarks
|
|
1908
|
+
* The event ID is used by the EventSource API to resume the stream
|
|
1909
|
+
* from the last received event after a reconnection. The browser
|
|
1910
|
+
* automatically sends this ID in the `Last-Event-ID` header.
|
|
1911
|
+
*
|
|
1912
|
+
* @example
|
|
1913
|
+
* ```typescript
|
|
1914
|
+
* const lastId = connection.getLastEventId();
|
|
1915
|
+
* if (lastId) {
|
|
1916
|
+
* console.log(`Last event: ${lastId}`);
|
|
1917
|
+
* }
|
|
1918
|
+
* ```
|
|
1919
|
+
*/
|
|
1920
|
+
getLastEventId(): string | null;
|
|
1921
|
+
/**
|
|
1922
|
+
* Setup event listeners for configured event types.
|
|
1923
|
+
*/
|
|
1924
|
+
private setupEventListeners;
|
|
1925
|
+
/**
|
|
1926
|
+
* Handle incoming message event.
|
|
1927
|
+
*/
|
|
1928
|
+
private handleMessage;
|
|
1929
|
+
/**
|
|
1930
|
+
* Handle error event from EventSource.
|
|
1931
|
+
*/
|
|
1932
|
+
private handleError;
|
|
1933
|
+
/**
|
|
1934
|
+
* Close EventSource and clean up.
|
|
1935
|
+
*/
|
|
1936
|
+
private closeEventSource;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
/**
|
|
1940
|
+
* @file WebSocket connection implementation
|
|
1941
|
+
* @module @maplibre-yaml/core/data/streaming
|
|
1942
|
+
*/
|
|
1943
|
+
|
|
1944
|
+
/**
|
|
1945
|
+
* Configuration for WebSocket connections.
|
|
1946
|
+
*
|
|
1947
|
+
* @remarks
|
|
1948
|
+
* WebSocket provides full-duplex communication over a single TCP connection,
|
|
1949
|
+
* enabling bidirectional data flow between client and server.
|
|
1950
|
+
*/
|
|
1951
|
+
interface WebSocketConfig extends ConnectionConfig {
|
|
1952
|
+
/**
|
|
1953
|
+
* WebSocket sub-protocols to use
|
|
1954
|
+
*
|
|
1955
|
+
* @remarks
|
|
1956
|
+
* Sub-protocols allow the client and server to agree on a specific
|
|
1957
|
+
* protocol on top of WebSocket. Can be a single string or array of strings.
|
|
1958
|
+
*
|
|
1959
|
+
* @example
|
|
1960
|
+
* ```typescript
|
|
1961
|
+
* protocols: 'json'
|
|
1962
|
+
* protocols: ['json', 'msgpack']
|
|
1963
|
+
* ```
|
|
1964
|
+
*/
|
|
1965
|
+
protocols?: string | string[];
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* WebSocket connection for bidirectional streaming.
|
|
1969
|
+
*
|
|
1970
|
+
* @remarks
|
|
1971
|
+
* Provides real-time bidirectional communication with automatic reconnection.
|
|
1972
|
+
* Unlike SSE, WebSocket supports sending data from client to server.
|
|
1973
|
+
*
|
|
1974
|
+
* Features:
|
|
1975
|
+
* - Full-duplex communication
|
|
1976
|
+
* - JSON message parsing with text fallback
|
|
1977
|
+
* - Manual reconnection with exponential backoff
|
|
1978
|
+
* - Sub-protocol support
|
|
1979
|
+
* - Send capability with connection validation
|
|
1980
|
+
*
|
|
1981
|
+
* @example
|
|
1982
|
+
* ```typescript
|
|
1983
|
+
* const ws = new WebSocketConnection({
|
|
1984
|
+
* url: 'wss://api.example.com/stream',
|
|
1985
|
+
* protocols: 'json',
|
|
1986
|
+
* });
|
|
1987
|
+
*
|
|
1988
|
+
* ws.on('message', ({ data }) => {
|
|
1989
|
+
* console.log('Received:', data);
|
|
1990
|
+
* });
|
|
1991
|
+
*
|
|
1992
|
+
* await ws.connect();
|
|
1993
|
+
* ws.send({ type: 'subscribe', channel: 'updates' });
|
|
1994
|
+
* ```
|
|
1995
|
+
*/
|
|
1996
|
+
declare class WebSocketConnection extends BaseConnection {
|
|
1997
|
+
private ws;
|
|
1998
|
+
private readonly wsConfig;
|
|
1999
|
+
/**
|
|
2000
|
+
* Create a new WebSocket connection.
|
|
2001
|
+
*
|
|
2002
|
+
* @param config - WebSocket configuration
|
|
2003
|
+
*
|
|
2004
|
+
* @example
|
|
2005
|
+
* ```typescript
|
|
2006
|
+
* const connection = new WebSocketConnection({
|
|
2007
|
+
* url: 'wss://api.example.com/stream',
|
|
2008
|
+
* protocols: ['json', 'v1'],
|
|
2009
|
+
* });
|
|
2010
|
+
* ```
|
|
2011
|
+
*/
|
|
2012
|
+
constructor(config: WebSocketConfig);
|
|
2013
|
+
/**
|
|
2014
|
+
* Establish WebSocket connection.
|
|
2015
|
+
*
|
|
2016
|
+
* @remarks
|
|
2017
|
+
* Creates a WebSocket and sets up event listeners for:
|
|
2018
|
+
* - Connection open
|
|
2019
|
+
* - Message reception
|
|
2020
|
+
* - Connection close
|
|
2021
|
+
* - Errors
|
|
2022
|
+
*
|
|
2023
|
+
* Unlike EventSource, WebSocket does not have built-in reconnection,
|
|
2024
|
+
* so reconnection is handled manually via the BaseConnection.
|
|
2025
|
+
*
|
|
2026
|
+
* @throws Error if WebSocket is not supported or connection fails
|
|
2027
|
+
*
|
|
2028
|
+
* @example
|
|
2029
|
+
* ```typescript
|
|
2030
|
+
* await connection.connect();
|
|
2031
|
+
* console.log('Connected to WebSocket');
|
|
2032
|
+
* ```
|
|
2033
|
+
*/
|
|
2034
|
+
connect(): Promise<void>;
|
|
2035
|
+
/**
|
|
2036
|
+
* Close WebSocket connection.
|
|
2037
|
+
*
|
|
2038
|
+
* @remarks
|
|
2039
|
+
* Closes the WebSocket with a normal closure code (1000).
|
|
2040
|
+
* Sets the manual disconnect flag to prevent automatic reconnection.
|
|
2041
|
+
*
|
|
2042
|
+
* @example
|
|
2043
|
+
* ```typescript
|
|
2044
|
+
* connection.disconnect();
|
|
2045
|
+
* console.log('Disconnected from WebSocket');
|
|
2046
|
+
* ```
|
|
2047
|
+
*/
|
|
2048
|
+
disconnect(): void;
|
|
2049
|
+
/**
|
|
2050
|
+
* Send data through WebSocket.
|
|
2051
|
+
*
|
|
2052
|
+
* @param data - Data to send (will be JSON stringified)
|
|
2053
|
+
* @throws Error if not connected
|
|
2054
|
+
*
|
|
2055
|
+
* @remarks
|
|
2056
|
+
* The data is automatically converted to JSON before sending.
|
|
2057
|
+
* Throws an error if called when the connection is not established.
|
|
2058
|
+
*
|
|
2059
|
+
* @example
|
|
2060
|
+
* ```typescript
|
|
2061
|
+
* connection.send({ type: 'ping' });
|
|
2062
|
+
* connection.send({ type: 'subscribe', channel: 'updates' });
|
|
2063
|
+
* ```
|
|
2064
|
+
*/
|
|
2065
|
+
send(data: unknown): void;
|
|
2066
|
+
/**
|
|
2067
|
+
* Setup WebSocket event listeners.
|
|
2068
|
+
*/
|
|
2069
|
+
private setupEventListeners;
|
|
2070
|
+
/**
|
|
2071
|
+
* Handle incoming message event.
|
|
2072
|
+
*/
|
|
2073
|
+
private handleMessage;
|
|
2074
|
+
/**
|
|
2075
|
+
* Handle close event from WebSocket.
|
|
2076
|
+
*/
|
|
2077
|
+
private handleClose;
|
|
2078
|
+
/**
|
|
2079
|
+
* Handle error event from WebSocket.
|
|
2080
|
+
*/
|
|
2081
|
+
private handleError;
|
|
2082
|
+
/**
|
|
2083
|
+
* Close WebSocket and clean up.
|
|
2084
|
+
*/
|
|
2085
|
+
private closeWebSocket;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
/**
|
|
2089
|
+
* Configuration for a streaming connection.
|
|
2090
|
+
*
|
|
2091
|
+
* @example
|
|
2092
|
+
* ```typescript
|
|
2093
|
+
* const config: StreamConfig = {
|
|
2094
|
+
* type: 'sse',
|
|
2095
|
+
* url: 'https://api.example.com/events',
|
|
2096
|
+
* onData: (data) => console.log('Received:', data),
|
|
2097
|
+
* reconnect: { enabled: true, maxRetries: 10 }
|
|
2098
|
+
* };
|
|
2099
|
+
* ```
|
|
2100
|
+
*/
|
|
2101
|
+
interface StreamConfig {
|
|
2102
|
+
/** Type of streaming connection */
|
|
2103
|
+
type: "websocket" | "sse";
|
|
2104
|
+
/** URL for the streaming connection */
|
|
2105
|
+
url: string;
|
|
2106
|
+
/** Callback for incoming data */
|
|
2107
|
+
onData: (data: FeatureCollection) => void;
|
|
2108
|
+
/** Callback for connection state changes */
|
|
2109
|
+
onStateChange?: (state: ConnectionState) => void;
|
|
2110
|
+
/** Callback for errors */
|
|
2111
|
+
onError?: (error: Error) => void;
|
|
2112
|
+
/** Reconnection configuration */
|
|
2113
|
+
reconnect?: {
|
|
2114
|
+
enabled?: boolean;
|
|
2115
|
+
maxRetries?: number;
|
|
2116
|
+
initialDelay?: number;
|
|
2117
|
+
maxDelay?: number;
|
|
2118
|
+
};
|
|
2119
|
+
/** Event types to listen for (SSE only) */
|
|
2120
|
+
eventTypes?: string[];
|
|
2121
|
+
/** WebSocket protocols (WebSocket only) */
|
|
2122
|
+
protocols?: string | string[];
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* State of a streaming connection.
|
|
2126
|
+
*/
|
|
2127
|
+
interface StreamState {
|
|
2128
|
+
/** Current connection state */
|
|
2129
|
+
connectionState: ConnectionState;
|
|
2130
|
+
/** Number of messages received */
|
|
2131
|
+
messageCount: number;
|
|
2132
|
+
/** Timestamp of last message (milliseconds) */
|
|
2133
|
+
lastMessage: number | null;
|
|
2134
|
+
/** Number of reconnection attempts */
|
|
2135
|
+
reconnectAttempts: number;
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* Manages streaming connections (SSE and WebSocket).
|
|
2139
|
+
*
|
|
2140
|
+
* @remarks
|
|
2141
|
+
* Provides a unified interface for managing multiple streaming connections,
|
|
2142
|
+
* handling connection lifecycle, automatic reconnection, and message routing.
|
|
2143
|
+
*
|
|
2144
|
+
* Features:
|
|
2145
|
+
* - Multiple concurrent connections
|
|
2146
|
+
* - Automatic reconnection with exponential backoff
|
|
2147
|
+
* - Connection state tracking
|
|
2148
|
+
* - Message counting and statistics
|
|
2149
|
+
* - Type-safe callbacks
|
|
2150
|
+
*
|
|
2151
|
+
* @example
|
|
2152
|
+
* ```typescript
|
|
2153
|
+
* const manager = new StreamManager();
|
|
2154
|
+
*
|
|
2155
|
+
* // Connect to SSE stream
|
|
2156
|
+
* await manager.connect('earthquake-feed', {
|
|
2157
|
+
* type: 'sse',
|
|
2158
|
+
* url: 'https://earthquake.usgs.gov/events',
|
|
2159
|
+
* onData: (data) => updateMap(data),
|
|
2160
|
+
* reconnect: { enabled: true }
|
|
2161
|
+
* });
|
|
2162
|
+
*
|
|
2163
|
+
* // Connect to WebSocket stream
|
|
2164
|
+
* await manager.connect('vehicle-updates', {
|
|
2165
|
+
* type: 'websocket',
|
|
2166
|
+
* url: 'wss://transit.example.com/vehicles',
|
|
2167
|
+
* onData: (data) => updateVehicles(data),
|
|
2168
|
+
* protocols: ['json']
|
|
2169
|
+
* });
|
|
2170
|
+
*
|
|
2171
|
+
* // Send data via WebSocket
|
|
2172
|
+
* manager.send('vehicle-updates', { type: 'subscribe', channel: 'all' });
|
|
2173
|
+
*
|
|
2174
|
+
* // Check connection state
|
|
2175
|
+
* const state = manager.getState('earthquake-feed');
|
|
2176
|
+
* console.log(`Messages received: ${state?.messageCount}`);
|
|
2177
|
+
*
|
|
2178
|
+
* // Disconnect when done
|
|
2179
|
+
* manager.disconnect('earthquake-feed');
|
|
2180
|
+
* manager.disconnectAll();
|
|
2181
|
+
* ```
|
|
2182
|
+
*/
|
|
2183
|
+
declare class StreamManager {
|
|
2184
|
+
private subscriptions;
|
|
2185
|
+
/**
|
|
2186
|
+
* Connect to a streaming source.
|
|
2187
|
+
*
|
|
2188
|
+
* @param id - Unique identifier for this connection
|
|
2189
|
+
* @param config - Stream configuration
|
|
2190
|
+
* @throws {Error} If a connection with the given id already exists
|
|
2191
|
+
*
|
|
2192
|
+
* @example
|
|
2193
|
+
* ```typescript
|
|
2194
|
+
* await manager.connect('updates', {
|
|
2195
|
+
* type: 'sse',
|
|
2196
|
+
* url: 'https://api.example.com/stream',
|
|
2197
|
+
* onData: (data) => console.log(data)
|
|
2198
|
+
* });
|
|
2199
|
+
* ```
|
|
2200
|
+
*/
|
|
2201
|
+
connect(id: string, config: StreamConfig): Promise<void>;
|
|
2202
|
+
/**
|
|
2203
|
+
* Disconnect a specific stream.
|
|
2204
|
+
*
|
|
2205
|
+
* @param id - Stream identifier
|
|
2206
|
+
*
|
|
2207
|
+
* @example
|
|
2208
|
+
* ```typescript
|
|
2209
|
+
* manager.disconnect('updates');
|
|
2210
|
+
* ```
|
|
2211
|
+
*/
|
|
2212
|
+
disconnect(id: string): void;
|
|
2213
|
+
/**
|
|
2214
|
+
* Disconnect all active streams.
|
|
2215
|
+
*
|
|
2216
|
+
* @example
|
|
2217
|
+
* ```typescript
|
|
2218
|
+
* manager.disconnectAll();
|
|
2219
|
+
* ```
|
|
2220
|
+
*/
|
|
2221
|
+
disconnectAll(): void;
|
|
2222
|
+
/**
|
|
2223
|
+
* Get the current state of a stream.
|
|
2224
|
+
*
|
|
2225
|
+
* @param id - Stream identifier
|
|
2226
|
+
* @returns Stream state or null if not found
|
|
2227
|
+
*
|
|
2228
|
+
* @example
|
|
2229
|
+
* ```typescript
|
|
2230
|
+
* const state = manager.getState('updates');
|
|
2231
|
+
* if (state) {
|
|
2232
|
+
* console.log(`State: ${state.connectionState}`);
|
|
2233
|
+
* console.log(`Messages: ${state.messageCount}`);
|
|
2234
|
+
* }
|
|
2235
|
+
* ```
|
|
2236
|
+
*/
|
|
2237
|
+
getState(id: string): StreamState | null;
|
|
2238
|
+
/**
|
|
2239
|
+
* Check if a stream is currently connected.
|
|
2240
|
+
*
|
|
2241
|
+
* @param id - Stream identifier
|
|
2242
|
+
* @returns True if connected, false otherwise
|
|
2243
|
+
*
|
|
2244
|
+
* @example
|
|
2245
|
+
* ```typescript
|
|
2246
|
+
* if (manager.isConnected('updates')) {
|
|
2247
|
+
* console.log('Stream is active');
|
|
2248
|
+
* }
|
|
2249
|
+
* ```
|
|
2250
|
+
*/
|
|
2251
|
+
isConnected(id: string): boolean;
|
|
2252
|
+
/**
|
|
2253
|
+
* Get all active stream IDs.
|
|
2254
|
+
*
|
|
2255
|
+
* @returns Array of active stream identifiers
|
|
2256
|
+
*
|
|
2257
|
+
* @example
|
|
2258
|
+
* ```typescript
|
|
2259
|
+
* const activeStreams = manager.getActiveIds();
|
|
2260
|
+
* console.log(`Active streams: ${activeStreams.join(', ')}`);
|
|
2261
|
+
* ```
|
|
2262
|
+
*/
|
|
2263
|
+
getActiveIds(): string[];
|
|
2264
|
+
/**
|
|
2265
|
+
* Send data to a WebSocket connection.
|
|
2266
|
+
*
|
|
2267
|
+
* @param id - Stream identifier
|
|
2268
|
+
* @param data - Data to send (will be JSON stringified)
|
|
2269
|
+
* @throws {Error} If stream is not a WebSocket connection or not connected
|
|
2270
|
+
*
|
|
2271
|
+
* @example
|
|
2272
|
+
* ```typescript
|
|
2273
|
+
* manager.send('ws-updates', {
|
|
2274
|
+
* type: 'subscribe',
|
|
2275
|
+
* channels: ['news', 'sports']
|
|
2276
|
+
* });
|
|
2277
|
+
* ```
|
|
2278
|
+
*/
|
|
2279
|
+
send(id: string, data: unknown): void;
|
|
2280
|
+
/**
|
|
2281
|
+
* Clean up all resources.
|
|
2282
|
+
*
|
|
2283
|
+
* @example
|
|
2284
|
+
* ```typescript
|
|
2285
|
+
* manager.destroy();
|
|
2286
|
+
* ```
|
|
2287
|
+
*/
|
|
2288
|
+
destroy(): void;
|
|
2289
|
+
/**
|
|
2290
|
+
* Create a connection instance based on config type.
|
|
2291
|
+
*/
|
|
2292
|
+
private createConnection;
|
|
2293
|
+
/**
|
|
2294
|
+
* Setup event handlers for a connection.
|
|
2295
|
+
*/
|
|
2296
|
+
private setupEventHandlers;
|
|
2297
|
+
/**
|
|
2298
|
+
* Type guard to check if data is a FeatureCollection.
|
|
2299
|
+
*/
|
|
2300
|
+
private isFeatureCollection;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
/**
|
|
2304
|
+
* Strategy for merging data.
|
|
2305
|
+
*/
|
|
2306
|
+
type MergeStrategy = "replace" | "merge" | "append-window";
|
|
2307
|
+
/**
|
|
2308
|
+
* Options for merging data.
|
|
2309
|
+
*
|
|
2310
|
+
* @example
|
|
2311
|
+
* ```typescript
|
|
2312
|
+
* // Replace strategy
|
|
2313
|
+
* const replaceOptions: MergeOptions = {
|
|
2314
|
+
* strategy: 'replace'
|
|
2315
|
+
* };
|
|
2316
|
+
*
|
|
2317
|
+
* // Merge strategy with update key
|
|
2318
|
+
* const mergeOptions: MergeOptions = {
|
|
2319
|
+
* strategy: 'merge',
|
|
2320
|
+
* updateKey: 'id'
|
|
2321
|
+
* };
|
|
2322
|
+
*
|
|
2323
|
+
* // Append-window with size and time limits
|
|
2324
|
+
* const windowOptions: MergeOptions = {
|
|
2325
|
+
* strategy: 'append-window',
|
|
2326
|
+
* windowSize: 100,
|
|
2327
|
+
* windowDuration: 3600000, // 1 hour
|
|
2328
|
+
* timestampField: 'timestamp'
|
|
2329
|
+
* };
|
|
2330
|
+
* ```
|
|
2331
|
+
*/
|
|
2332
|
+
interface MergeOptions {
|
|
2333
|
+
/** Merge strategy to use */
|
|
2334
|
+
strategy: MergeStrategy;
|
|
2335
|
+
/** Property key used to identify features for merge strategy */
|
|
2336
|
+
updateKey?: string;
|
|
2337
|
+
/** Maximum number of features to keep (append-window) */
|
|
2338
|
+
windowSize?: number;
|
|
2339
|
+
/** Maximum age of features in milliseconds (append-window) */
|
|
2340
|
+
windowDuration?: number;
|
|
2341
|
+
/** Property field containing timestamp (append-window) */
|
|
2342
|
+
timestampField?: string;
|
|
2343
|
+
}
|
|
2344
|
+
/**
|
|
2345
|
+
* Result of a merge operation.
|
|
2346
|
+
*/
|
|
2347
|
+
interface MergeResult {
|
|
2348
|
+
/** Merged feature collection */
|
|
2349
|
+
data: FeatureCollection;
|
|
2350
|
+
/** Number of features added */
|
|
2351
|
+
added: number;
|
|
2352
|
+
/** Number of features updated */
|
|
2353
|
+
updated: number;
|
|
2354
|
+
/** Number of features removed */
|
|
2355
|
+
removed: number;
|
|
2356
|
+
/** Total number of features in result */
|
|
2357
|
+
total: number;
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Merges GeoJSON FeatureCollections using configurable strategies.
|
|
2361
|
+
*
|
|
2362
|
+
* @remarks
|
|
2363
|
+
* Supports three merge strategies:
|
|
2364
|
+
*
|
|
2365
|
+
* **replace**: Complete replacement of existing data
|
|
2366
|
+
* - Replaces all existing features with incoming features
|
|
2367
|
+
* - Simple and efficient for full updates
|
|
2368
|
+
*
|
|
2369
|
+
* **merge**: Update by key, keep unmatched
|
|
2370
|
+
* - Updates existing features by matching on a key property
|
|
2371
|
+
* - Adds new features that don't match existing keys
|
|
2372
|
+
* - Preserves existing features not in the update
|
|
2373
|
+
* - Requires `updateKey` option
|
|
2374
|
+
*
|
|
2375
|
+
* **append-window**: Add with time/size limits
|
|
2376
|
+
* - Appends incoming features to existing features
|
|
2377
|
+
* - Applies size limit (keeps most recent N features)
|
|
2378
|
+
* - Applies time limit (removes features older than duration)
|
|
2379
|
+
* - Requires `timestampField` for time-based filtering
|
|
2380
|
+
*
|
|
2381
|
+
* @example
|
|
2382
|
+
* ```typescript
|
|
2383
|
+
* const merger = new DataMerger();
|
|
2384
|
+
*
|
|
2385
|
+
* // Replace all data
|
|
2386
|
+
* const result = merger.merge(existing, incoming, {
|
|
2387
|
+
* strategy: 'replace'
|
|
2388
|
+
* });
|
|
2389
|
+
*
|
|
2390
|
+
* // Merge by vehicle ID
|
|
2391
|
+
* const result = merger.merge(existing, incoming, {
|
|
2392
|
+
* strategy: 'merge',
|
|
2393
|
+
* updateKey: 'vehicleId'
|
|
2394
|
+
* });
|
|
2395
|
+
*
|
|
2396
|
+
* // Append with 100 feature limit and 1 hour window
|
|
2397
|
+
* const result = merger.merge(existing, incoming, {
|
|
2398
|
+
* strategy: 'append-window',
|
|
2399
|
+
* windowSize: 100,
|
|
2400
|
+
* windowDuration: 3600000,
|
|
2401
|
+
* timestampField: 'timestamp'
|
|
2402
|
+
* });
|
|
2403
|
+
* ```
|
|
2404
|
+
*/
|
|
2405
|
+
declare class DataMerger {
|
|
2406
|
+
/**
|
|
2407
|
+
* Merge two FeatureCollections using the specified strategy.
|
|
2408
|
+
*
|
|
2409
|
+
* @param existing - Existing feature collection
|
|
2410
|
+
* @param incoming - Incoming feature collection to merge
|
|
2411
|
+
* @param options - Merge options including strategy
|
|
2412
|
+
* @returns Merge result with statistics
|
|
2413
|
+
* @throws {Error} If merge strategy requires missing options
|
|
2414
|
+
*
|
|
2415
|
+
* @example
|
|
2416
|
+
* ```typescript
|
|
2417
|
+
* const merger = new DataMerger();
|
|
2418
|
+
*
|
|
2419
|
+
* const result = merger.merge(existingData, newData, {
|
|
2420
|
+
* strategy: 'merge',
|
|
2421
|
+
* updateKey: 'id'
|
|
2422
|
+
* });
|
|
2423
|
+
*
|
|
2424
|
+
* console.log(`Added: ${result.added}, Updated: ${result.updated}`);
|
|
2425
|
+
* console.log(`Total features: ${result.total}`);
|
|
2426
|
+
* ```
|
|
2427
|
+
*/
|
|
2428
|
+
merge(existing: FeatureCollection, incoming: FeatureCollection, options: MergeOptions): MergeResult;
|
|
2429
|
+
/**
|
|
2430
|
+
* Replace strategy: Complete replacement of existing data.
|
|
2431
|
+
*/
|
|
2432
|
+
private mergeReplace;
|
|
2433
|
+
/**
|
|
2434
|
+
* Merge strategy: Update by key, keep unmatched features.
|
|
2435
|
+
*/
|
|
2436
|
+
private mergeMerge;
|
|
2437
|
+
/**
|
|
2438
|
+
* Append-window strategy: Add with time/size limits.
|
|
2439
|
+
*/
|
|
2440
|
+
private mergeAppendWindow;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
/**
|
|
2444
|
+
* Configuration for the loading manager.
|
|
2445
|
+
*/
|
|
2446
|
+
interface LoadingConfig {
|
|
2447
|
+
/** Whether to show loading UI overlays (default: false) */
|
|
2448
|
+
showUI: boolean;
|
|
2449
|
+
/** Custom messages for loading states */
|
|
2450
|
+
messages?: {
|
|
2451
|
+
loading?: string;
|
|
2452
|
+
error?: string;
|
|
2453
|
+
retry?: string;
|
|
2454
|
+
};
|
|
2455
|
+
/** Spinner style (default: 'circle') */
|
|
2456
|
+
spinnerStyle?: "circle" | "dots";
|
|
2457
|
+
/** Minimum time to display loading UI in milliseconds (default: 300) */
|
|
2458
|
+
minDisplayTime?: number;
|
|
2459
|
+
}
|
|
2460
|
+
/**
|
|
2461
|
+
* Events emitted by the loading manager.
|
|
2462
|
+
*/
|
|
2463
|
+
interface LoadingEvents extends Record<string, unknown> {
|
|
2464
|
+
"loading:start": {
|
|
2465
|
+
layerId: string;
|
|
2466
|
+
message?: string;
|
|
2467
|
+
};
|
|
2468
|
+
"loading:progress": {
|
|
2469
|
+
layerId: string;
|
|
2470
|
+
loaded: number;
|
|
2471
|
+
total?: number;
|
|
2472
|
+
};
|
|
2473
|
+
"loading:complete": {
|
|
2474
|
+
layerId: string;
|
|
2475
|
+
duration: number;
|
|
2476
|
+
fromCache: boolean;
|
|
2477
|
+
};
|
|
2478
|
+
"loading:error": {
|
|
2479
|
+
layerId: string;
|
|
2480
|
+
error: Error;
|
|
2481
|
+
retrying: boolean;
|
|
2482
|
+
};
|
|
2483
|
+
"loading:retry": {
|
|
2484
|
+
layerId: string;
|
|
2485
|
+
attempt: number;
|
|
2486
|
+
delay: number;
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
/**
|
|
2490
|
+
* State of a loading operation.
|
|
2491
|
+
*/
|
|
2492
|
+
interface LoadingState {
|
|
2493
|
+
/** Whether currently loading */
|
|
2494
|
+
isLoading: boolean;
|
|
2495
|
+
/** When loading started (timestamp) */
|
|
2496
|
+
startTime: number | null;
|
|
2497
|
+
/** Custom loading message */
|
|
2498
|
+
message?: string;
|
|
2499
|
+
/** Current error if any */
|
|
2500
|
+
error?: Error;
|
|
2501
|
+
/** Current retry attempt number */
|
|
2502
|
+
retryAttempt?: number;
|
|
2503
|
+
}
|
|
2504
|
+
/**
|
|
2505
|
+
* Manages loading states and optional UI overlays.
|
|
2506
|
+
*
|
|
2507
|
+
* @remarks
|
|
2508
|
+
* Provides centralized loading state management with optional visual feedback.
|
|
2509
|
+
* Emits events for all loading state changes, allowing external UI integration.
|
|
2510
|
+
* Can optionally show built-in loading overlays with spinners and error messages.
|
|
2511
|
+
*
|
|
2512
|
+
* Features:
|
|
2513
|
+
* - Event-driven state changes
|
|
2514
|
+
* - Optional loading UI overlays
|
|
2515
|
+
* - Customizable messages and spinner styles
|
|
2516
|
+
* - Minimum display time to prevent flashing
|
|
2517
|
+
* - Retry support with visual feedback
|
|
2518
|
+
* - Multiple concurrent loading operations
|
|
2519
|
+
*
|
|
2520
|
+
* @example
|
|
2521
|
+
* ```typescript
|
|
2522
|
+
* const manager = new LoadingManager({
|
|
2523
|
+
* showUI: true,
|
|
2524
|
+
* minDisplayTime: 300
|
|
2525
|
+
* });
|
|
2526
|
+
*
|
|
2527
|
+
* // Listen to loading events
|
|
2528
|
+
* manager.on('loading:start', ({ layerId }) => {
|
|
2529
|
+
* console.log(`Loading ${layerId}...`);
|
|
2530
|
+
* });
|
|
2531
|
+
*
|
|
2532
|
+
* // Show loading state
|
|
2533
|
+
* const container = document.getElementById('map-container');
|
|
2534
|
+
* manager.showLoading('vehicles', container, 'Loading vehicle data...');
|
|
2535
|
+
*
|
|
2536
|
+
* // Hide when complete
|
|
2537
|
+
* manager.hideLoading('vehicles', { fromCache: false });
|
|
2538
|
+
*
|
|
2539
|
+
* // Show error with retry
|
|
2540
|
+
* manager.showError('vehicles', container, new Error('Failed'), () => {
|
|
2541
|
+
* // Retry logic
|
|
2542
|
+
* });
|
|
2543
|
+
* ```
|
|
2544
|
+
*/
|
|
2545
|
+
declare class LoadingManager extends EventEmitter<LoadingEvents> {
|
|
2546
|
+
private config;
|
|
2547
|
+
private subscriptions;
|
|
2548
|
+
private static readonly DEFAULT_CONFIG;
|
|
2549
|
+
/**
|
|
2550
|
+
* Create a new LoadingManager.
|
|
2551
|
+
*
|
|
2552
|
+
* @param config - Loading manager configuration
|
|
2553
|
+
*
|
|
2554
|
+
* @example
|
|
2555
|
+
* ```typescript
|
|
2556
|
+
* const manager = new LoadingManager({
|
|
2557
|
+
* showUI: true,
|
|
2558
|
+
* messages: {
|
|
2559
|
+
* loading: 'Fetching data...',
|
|
2560
|
+
* error: 'Could not load data'
|
|
2561
|
+
* },
|
|
2562
|
+
* spinnerStyle: 'dots',
|
|
2563
|
+
* minDisplayTime: 500
|
|
2564
|
+
* });
|
|
2565
|
+
* ```
|
|
2566
|
+
*/
|
|
2567
|
+
constructor(config?: Partial<LoadingConfig>);
|
|
2568
|
+
/**
|
|
2569
|
+
* Show loading state for a layer.
|
|
2570
|
+
*
|
|
2571
|
+
* @param layerId - Layer identifier
|
|
2572
|
+
* @param container - Container element for UI overlay
|
|
2573
|
+
* @param message - Custom loading message
|
|
2574
|
+
*
|
|
2575
|
+
* @example
|
|
2576
|
+
* ```typescript
|
|
2577
|
+
* const container = document.getElementById('map');
|
|
2578
|
+
* manager.showLoading('earthquakes', container, 'Loading earthquake data...');
|
|
2579
|
+
* ```
|
|
2580
|
+
*/
|
|
2581
|
+
showLoading(layerId: string, container: HTMLElement, message?: string): void;
|
|
2582
|
+
/**
|
|
2583
|
+
* Hide loading state for a layer.
|
|
2584
|
+
*
|
|
2585
|
+
* @param layerId - Layer identifier
|
|
2586
|
+
* @param result - Optional result information
|
|
2587
|
+
*
|
|
2588
|
+
* @example
|
|
2589
|
+
* ```typescript
|
|
2590
|
+
* manager.hideLoading('earthquakes', { fromCache: true });
|
|
2591
|
+
* ```
|
|
2592
|
+
*/
|
|
2593
|
+
hideLoading(layerId: string, result?: {
|
|
2594
|
+
fromCache: boolean;
|
|
2595
|
+
}): void;
|
|
2596
|
+
/**
|
|
2597
|
+
* Show error state for a layer.
|
|
2598
|
+
*
|
|
2599
|
+
* @param layerId - Layer identifier
|
|
2600
|
+
* @param container - Container element for UI overlay
|
|
2601
|
+
* @param error - Error that occurred
|
|
2602
|
+
* @param onRetry - Optional retry callback
|
|
2603
|
+
*
|
|
2604
|
+
* @example
|
|
2605
|
+
* ```typescript
|
|
2606
|
+
* manager.showError('earthquakes', container, error, () => {
|
|
2607
|
+
* // Retry loading
|
|
2608
|
+
* fetchData();
|
|
2609
|
+
* });
|
|
2610
|
+
* ```
|
|
2611
|
+
*/
|
|
2612
|
+
showError(layerId: string, container: HTMLElement, error: Error, onRetry?: () => void): void;
|
|
2613
|
+
/**
|
|
2614
|
+
* Show retrying state for a layer.
|
|
2615
|
+
*
|
|
2616
|
+
* @param layerId - Layer identifier
|
|
2617
|
+
* @param attempt - Current retry attempt number
|
|
2618
|
+
* @param delay - Delay before retry in milliseconds
|
|
2619
|
+
*
|
|
2620
|
+
* @example
|
|
2621
|
+
* ```typescript
|
|
2622
|
+
* manager.showRetrying('earthquakes', 2, 2000);
|
|
2623
|
+
* ```
|
|
2624
|
+
*/
|
|
2625
|
+
showRetrying(layerId: string, attempt: number, delay: number): void;
|
|
2626
|
+
/**
|
|
2627
|
+
* Get loading state for a layer.
|
|
2628
|
+
*
|
|
2629
|
+
* @param layerId - Layer identifier
|
|
2630
|
+
* @returns Loading state or null if not found
|
|
2631
|
+
*
|
|
2632
|
+
* @example
|
|
2633
|
+
* ```typescript
|
|
2634
|
+
* const state = manager.getState('earthquakes');
|
|
2635
|
+
* if (state?.isLoading) {
|
|
2636
|
+
* console.log('Still loading...');
|
|
2637
|
+
* }
|
|
2638
|
+
* ```
|
|
2639
|
+
*/
|
|
2640
|
+
getState(layerId: string): LoadingState | null;
|
|
2641
|
+
/**
|
|
2642
|
+
* Check if a layer is currently loading.
|
|
2643
|
+
*
|
|
2644
|
+
* @param layerId - Layer identifier
|
|
2645
|
+
* @returns True if loading, false otherwise
|
|
2646
|
+
*
|
|
2647
|
+
* @example
|
|
2648
|
+
* ```typescript
|
|
2649
|
+
* if (manager.isLoading('earthquakes')) {
|
|
2650
|
+
* console.log('Loading in progress');
|
|
2651
|
+
* }
|
|
2652
|
+
* ```
|
|
2653
|
+
*/
|
|
2654
|
+
isLoading(layerId: string): boolean;
|
|
2655
|
+
/**
|
|
2656
|
+
* Clear all loading states and UI.
|
|
2657
|
+
*
|
|
2658
|
+
* @example
|
|
2659
|
+
* ```typescript
|
|
2660
|
+
* manager.clearAll();
|
|
2661
|
+
* ```
|
|
2662
|
+
*/
|
|
2663
|
+
clearAll(): void;
|
|
2664
|
+
/**
|
|
2665
|
+
* Clean up all resources.
|
|
2666
|
+
*
|
|
2667
|
+
* @example
|
|
2668
|
+
* ```typescript
|
|
2669
|
+
* manager.destroy();
|
|
2670
|
+
* ```
|
|
2671
|
+
*/
|
|
2672
|
+
destroy(): void;
|
|
2673
|
+
/**
|
|
2674
|
+
* Create loading overlay element.
|
|
2675
|
+
*/
|
|
2676
|
+
private createLoadingOverlay;
|
|
2677
|
+
/**
|
|
2678
|
+
* Create error overlay element.
|
|
2679
|
+
*/
|
|
2680
|
+
private createErrorOverlay;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
/**
|
|
2684
|
+
* @file Loading UI styles
|
|
2685
|
+
* @module @maplibre-yaml/core/ui/styles
|
|
2686
|
+
*/
|
|
2687
|
+
/**
|
|
2688
|
+
* CSS styles for loading overlays and spinners.
|
|
2689
|
+
*
|
|
2690
|
+
* @remarks
|
|
2691
|
+
* Includes:
|
|
2692
|
+
* - Loading overlay with backdrop
|
|
2693
|
+
* - Circle spinner animation
|
|
2694
|
+
* - Dots spinner animation
|
|
2695
|
+
* - Error overlay with icon and retry button
|
|
2696
|
+
* - Dark mode support
|
|
2697
|
+
* - Reduced motion support
|
|
2698
|
+
*/
|
|
2699
|
+
declare const loadingStyles = "\n/* Loading Overlay */\n.mly-loading-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(255, 255, 255, 0.85);\n z-index: 1000;\n backdrop-filter: blur(2px);\n}\n\n.mly-loading-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n}\n\n.mly-loading-text {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n font-size: 14px;\n color: #374151;\n font-weight: 500;\n}\n\n/* Circle Spinner */\n.mly-spinner--circle {\n width: 40px;\n height: 40px;\n border: 3px solid #e5e7eb;\n border-top-color: #3b82f6;\n border-radius: 50%;\n animation: mly-spin 0.8s linear infinite;\n}\n\n@keyframes mly-spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n/* Dots Spinner */\n.mly-spinner--dots {\n display: flex;\n gap: 8px;\n}\n\n.mly-spinner--dots::before,\n.mly-spinner--dots::after {\n content: '';\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: #3b82f6;\n animation: mly-dots 1.4s infinite ease-in-out both;\n}\n\n.mly-spinner--dots::before {\n animation-delay: -0.32s;\n}\n\n.mly-spinner--dots::after {\n animation-delay: -0.16s;\n}\n\n@keyframes mly-dots {\n 0%, 80%, 100% {\n opacity: 0.3;\n transform: scale(0.8);\n }\n 40% {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n/* Error Overlay */\n.mly-loading-overlay--error {\n background: rgba(254, 242, 242, 0.95);\n}\n\n.mly-error-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n max-width: 300px;\n padding: 20px;\n text-align: center;\n}\n\n.mly-error-icon {\n font-size: 32px;\n line-height: 1;\n}\n\n.mly-error-text {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n font-size: 14px;\n color: #991b1b;\n font-weight: 500;\n}\n\n.mly-retry-button {\n padding: 8px 16px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n font-size: 14px;\n font-weight: 500;\n color: white;\n background: #dc2626;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n transition: background 0.2s ease;\n}\n\n.mly-retry-button:hover {\n background: #b91c1c;\n}\n\n.mly-retry-button:active {\n background: #991b1b;\n}\n\n/* Dark Mode Support */\n@media (prefers-color-scheme: dark) {\n .mly-loading-overlay {\n background: rgba(17, 24, 39, 0.85);\n }\n\n .mly-loading-text {\n color: #e5e7eb;\n }\n\n .mly-spinner--circle {\n border-color: #374151;\n border-top-color: #60a5fa;\n }\n\n .mly-spinner--dots::before,\n .mly-spinner--dots::after {\n background: #60a5fa;\n }\n\n .mly-loading-overlay--error {\n background: rgba(127, 29, 29, 0.95);\n }\n\n .mly-error-text {\n color: #fecaca;\n }\n}\n\n/* Reduced Motion Support */\n@media (prefers-reduced-motion: reduce) {\n .mly-spinner--circle {\n animation: none;\n border-top-color: #3b82f6;\n opacity: 0.7;\n }\n\n .mly-spinner--dots::before,\n .mly-spinner--dots::after {\n animation: none;\n opacity: 0.7;\n }\n\n .mly-retry-button {\n transition: none;\n }\n}\n";
|
|
2700
|
+
/**
|
|
2701
|
+
* Inject loading styles into the document.
|
|
2702
|
+
*
|
|
2703
|
+
* @remarks
|
|
2704
|
+
* Automatically called when the loading manager is first used.
|
|
2705
|
+
* Only injects styles once, even if called multiple times.
|
|
2706
|
+
*
|
|
2707
|
+
* @example
|
|
2708
|
+
* ```typescript
|
|
2709
|
+
* import { injectLoadingStyles } from '@maplibre-yaml/core/ui/styles';
|
|
2710
|
+
*
|
|
2711
|
+
* // Manually inject styles
|
|
2712
|
+
* injectLoadingStyles();
|
|
2713
|
+
* ```
|
|
2714
|
+
*/
|
|
2715
|
+
declare function injectLoadingStyles(): void;
|
|
2716
|
+
|
|
2717
|
+
export { BaseConnection, type CacheConfig, type CacheEntry, type CacheStats, type ConnectionConfig, type ConnectionEvents, type ConnectionState, ControlsConfigSchema, ControlsManager, DataFetcher, DataMerger, EventEmitter, type EventHandler, type EventHandlerCallbacks, type FetchOptions, type FetchResult, type FetcherConfig, LayerManager, type LayerManagerCallbacks, LayerSchema, type LoadingConfig, type LoadingEvents, LoadingManager, type LoadingState, MaxRetriesExceededError, MemoryCache, type MergeOptions, type MergeResult, type MergeStrategy, type ParseError, type ParseResult, type PollingConfig, PollingManager, type PollingState, PopupBuilder, PopupContentSchema, type RetryCallbacks, type RetryConfig, RetryManager, type RootConfig, RootSchema, type SSEConfig, SSEConnection, type StreamConfig, StreamManager, type StreamState, type WebSocketConfig, WebSocketConnection, YAMLParser, injectLoadingStyles, loadingStyles, parseYAMLConfig, safeParseYAMLConfig };
|