@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.
@@ -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 };