• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / loaders.gl / 25070233425

28 Apr 2026 06:20PM UTC coverage: 59.434% (+0.01%) from 59.423%
25070233425

push

github

web-flow
website: Add tabs for navigating between format docs (#3407)

11310 of 20887 branches covered (54.15%)

Branch coverage included in aggregate %.

89 of 136 new or added lines in 13 files covered. (65.44%)

1742 existing lines in 132 files now uncovered.

23500 of 37682 relevant lines covered (62.36%)

16296.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.91
/modules/deck-layers/src/vector-source-layer/vector-set.ts
1
// loaders.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import type {Viewport} from '@deck.gl/core';
6
import type {Schema} from '@loaders.gl/schema';
7
import type {
8
  GetFeaturesParameters,
9
  VectorSource,
10
  VectorSourceMetadata
11
} from '@loaders.gl/loader-utils';
12

13
/** Resolved feature-table type returned by the active vector source. */
14
type VectorSetData = Awaited<ReturnType<VectorSource['getFeatures']>>;
15

16
/** Mutable fetch options consumed by {@link VectorSet}. */
17
export type VectorSetOptions = {
18
  /** Source used to resolve viewport feature requests. */
19
  vectorSource: VectorSource;
20
  /** Named source layers included in each request. */
21
  layers: string | string[];
22
  /** Output CRS forwarded to the vector source. */
23
  crs?: string;
24
  /** Output format forwarded to the vector source. */
25
  format?: GetFeaturesParameters['format'];
26
  /** Debounce interval applied before issuing viewport requests. */
27
  debounceTime?: number;
28
};
29

30
/** Readable snapshot of the current vector-set state. */
31
export type VectorSetState = {
32
  /** True after metadata/schema resolve successfully and a current table is available. */
33
  isLoaded: boolean;
34
  /** True while the latest viewport request is in flight. */
35
  isLoading: boolean;
36
  /** Latest accepted viewport table. */
37
  data: VectorSetData | null;
38
  /** Resolved source schema when available. */
39
  schema: Schema | null;
40
  /** Resolved source metadata when available. */
41
  metadata: VectorSourceMetadata | null;
42
  /** Latest accepted error, if any. */
43
  error: Error | null;
44
};
45

46
/** Subscription callbacks emitted by {@link VectorSet}. */
47
export type VectorSetEvents = {
48
  /** Called when metadata/viewport loading starts or stops. */
49
  onLoadingStateChange?: (isLoading: boolean) => void;
50
  /** Called whenever any public state changes. */
51
  onUpdate?: () => void;
52
  /** Called when a viewport request resolves and becomes current. */
53
  onDataLoad?: (table: VectorSetData) => void;
54
  /** Called when metadata resolves. */
55
  onMetadataLoad?: (metadata: VectorSourceMetadata) => void;
56
  /** Called when schema resolves. */
57
  onSchemaLoad?: (schema: Schema) => void;
58
  /** Called when the current metadata or viewport request fails. */
59
  onError?: (error: Error) => void;
60
};
61

62
/**
63
 * Small runtime helper that keeps the latest vector table in sync with the active viewport.
64
 *
65
 * `VectorSet` only accepts the most recent viewport request result and ignores stale responses.
66
 */
67
export class VectorSet {
68
  /** Current source and request options. */
69
  vectorSource: VectorSource;
70
  /** Layers forwarded to `VectorSource#getFeatures`. */
71
  layers: string | string[];
72
  /** Output CRS forwarded to `VectorSource#getFeatures`. */
73
  crs?: string;
74
  /** Output format forwarded to `VectorSource#getFeatures`. */
75
  format?: GetFeaturesParameters['format'];
76
  /** Debounce interval applied before issuing viewport requests. */
77
  debounceTime: number;
78

79
  /** True once the latest accepted viewport request completes successfully. */
UNCOV
80
  isLoaded = false;
9✔
81
  /** True while the latest viewport request is in flight. */
UNCOV
82
  isLoading = false;
9✔
83
  /** Latest accepted viewport table. */
UNCOV
84
  data: VectorSetData | null = null;
9✔
85
  /** Resolved source schema. */
UNCOV
86
  schema: Schema | null = null;
9✔
87
  /** Resolved source metadata. */
UNCOV
88
  metadata: VectorSourceMetadata | null = null;
9✔
89
  /** Latest accepted error. */
UNCOV
90
  error: Error | null = null;
9✔
91

UNCOV
92
  private readonly subscriptions = new Set<VectorSetEvents>();
9✔
UNCOV
93
  private metadataPromise: Promise<void> | null = null;
9✔
UNCOV
94
  private requestSequenceNumber = 0;
9✔
UNCOV
95
  private lastRequestKey: string | null = null;
9✔
UNCOV
96
  private loadCounter = 0;
9✔
UNCOV
97
  private timeoutId: ReturnType<typeof setTimeout> | null = null;
9✔
UNCOV
98
  private pendingTimeoutResolve: (() => void) | null = null;
9✔
UNCOV
99
  private abortController: AbortController | null = null;
9✔
100

101
  /** Creates a new viewport-driven vector runtime for a source. */
102
  constructor(options: VectorSetOptions) {
UNCOV
103
    this.vectorSource = options.vectorSource;
9✔
UNCOV
104
    this.layers = options.layers;
9✔
UNCOV
105
    this.crs = options.crs;
9✔
UNCOV
106
    this.format = options.format;
9✔
UNCOV
107
    this.debounceTime = options.debounceTime ?? 200;
9!
108
  }
109

110
  /** Creates a `VectorSet` from a vector source instance. */
111
  static fromVectorSource(
112
    vectorSource: VectorSource,
113
    options: Omit<VectorSetOptions, 'vectorSource'>
114
  ) {
UNCOV
115
    return new VectorSet({vectorSource, ...options});
3✔
116
  }
117

118
  /** Returns a readable snapshot for tests and consumers. */
119
  getState(): VectorSetState {
120
    return {
×
121
      isLoaded: this.isLoaded,
122
      isLoading: this.isLoading,
123
      data: this.data,
124
      schema: this.schema,
125
      metadata: this.metadata,
126
      error: this.error
127
    };
128
  }
129

130
  /** Updates the source or request options used for subsequent viewport fetches. */
131
  setOptions(options: VectorSetOptions): void {
UNCOV
132
    const sourceChanged = options.vectorSource !== this.vectorSource;
3✔
UNCOV
133
    const layersChanged = !areLayerSelectionsEqual(options.layers, this.layers);
3✔
UNCOV
134
    const crsChanged = options.crs !== this.crs;
3✔
UNCOV
135
    const formatChanged = options.format !== this.format;
3✔
136

UNCOV
137
    this.vectorSource = options.vectorSource;
3✔
UNCOV
138
    this.layers = options.layers;
3✔
UNCOV
139
    this.crs = options.crs;
3✔
UNCOV
140
    this.format = options.format;
3✔
UNCOV
141
    this.debounceTime = options.debounceTime ?? this.debounceTime;
3!
142

UNCOV
143
    if (sourceChanged) {
3!
144
      this._cancelScheduledRequest();
×
145
      this.metadataPromise = null;
×
146
      this.schema = null;
×
147
      this.metadata = null;
×
148
      this.data = null;
×
149
      this.error = null;
×
150
      this.isLoaded = false;
×
151
      this.isLoading = false;
×
152
      this.loadCounter = 0;
×
153
      this.lastRequestKey = null;
×
154
      this.requestSequenceNumber++;
×
155
      this.emitUpdate();
×
156
      return;
×
157
    }
158

UNCOV
159
    if (layersChanged || crsChanged || formatChanged) {
3!
160
      this.lastRequestKey = null;
×
161
    }
162
  }
163

164
  /** Loads metadata and schema once for the current source. */
165
  async loadMetadata(): Promise<void> {
UNCOV
166
    this.metadataPromise ||= this._loadMetadata();
3✔
UNCOV
167
    await this.metadataPromise;
3✔
168
  }
169

170
  /** Requests features for the supplied viewport when inputs changed. */
171
  async updateViewport(viewport: Viewport): Promise<void> {
UNCOV
172
    const requestParameters = this._getRequestParameters(viewport);
15✔
UNCOV
173
    const requestKey = getRequestKey(requestParameters);
15✔
174

UNCOV
175
    if (requestKey === this.lastRequestKey) {
15✔
UNCOV
176
      return;
1✔
177
    }
178

UNCOV
179
    this._cancelScheduledRequest();
14✔
UNCOV
180
    this._abortActiveRequest();
14✔
181

UNCOV
182
    if (this.debounceTime > 0) {
14✔
UNCOV
183
      await new Promise<void>(resolve => {
4✔
UNCOV
184
        this.pendingTimeoutResolve = resolve;
4✔
UNCOV
185
        this.timeoutId = setTimeout(() => {
4✔
UNCOV
186
          this.timeoutId = null;
2✔
UNCOV
187
          this.pendingTimeoutResolve = null;
2✔
UNCOV
188
          void this._loadFeatures(requestParameters, requestKey).finally(resolve);
2✔
189
        }, this.debounceTime);
190
      });
UNCOV
191
      return;
4✔
192
    }
193

UNCOV
194
    await this._loadFeatures(requestParameters, requestKey);
10✔
195
  }
196

197
  /** Subscribes to runtime state changes. */
198
  subscribe(events: VectorSetEvents): () => void {
UNCOV
199
    this.subscriptions.add(events);
5✔
UNCOV
200
    return () => this.subscriptions.delete(events);
5✔
201
  }
202

203
  /** Releases references and cancels acceptance of any in-flight request. */
204
  finalize(): void {
205
    this.requestSequenceNumber++;
×
206
    this._cancelScheduledRequest();
×
207
    this._abortActiveRequest();
×
208
    this.subscriptions.clear();
×
209
  }
210

211
  private async _loadMetadata(): Promise<void> {
UNCOV
212
    this._startLoading();
3✔
UNCOV
213
    try {
3✔
UNCOV
214
      const [metadata, schema] = await Promise.all([
3✔
215
        this.vectorSource.getMetadata({formatSpecificMetadata: false}),
216
        this.vectorSource.getSchema()
217
      ]);
218

UNCOV
219
      this.metadata = metadata;
3✔
UNCOV
220
      this.schema = schema;
3✔
UNCOV
221
      this.emitMetadataLoad(metadata);
3✔
UNCOV
222
      this.emitSchemaLoad(schema);
3✔
UNCOV
223
      this.emitUpdate();
3✔
224
    } catch (error) {
225
      this.error = normalizeError(error);
×
226
      this.emitError(this.error);
×
227
      this.emitUpdate();
×
228
      throw this.error;
×
229
    } finally {
UNCOV
230
      this._finishLoading();
3✔
231
    }
232
  }
233

234
  /** Issues the actual vector source request after debounce completes. */
235
  private async _loadFeatures(
236
    requestParameters: GetFeaturesParameters,
237
    requestKey: string
238
  ): Promise<void> {
UNCOV
239
    this.lastRequestKey = requestKey;
12✔
UNCOV
240
    const requestSequenceNumber = ++this.requestSequenceNumber;
12✔
UNCOV
241
    const abortController = new AbortController();
12✔
242

UNCOV
243
    this._startLoading();
12✔
UNCOV
244
    this.error = null;
12✔
UNCOV
245
    this.emitUpdate();
12✔
246

UNCOV
247
    try {
12✔
UNCOV
248
      this.abortController = abortController;
12✔
UNCOV
249
      const table = await this.vectorSource.getFeatures({
12✔
250
        ...requestParameters,
251
        signal: abortController.signal
252
      });
UNCOV
253
      if (requestSequenceNumber !== this.requestSequenceNumber) {
11✔
UNCOV
254
        return;
2✔
255
      }
256

UNCOV
257
      this.data = table;
9✔
UNCOV
258
      this.error = null;
9✔
UNCOV
259
      this.isLoaded = true;
9✔
UNCOV
260
      this.emitDataLoad(table);
9✔
UNCOV
261
      this.emitUpdate();
9✔
262
    } catch (error) {
UNCOV
263
      if (isAbortError(error)) {
1!
264
        return;
×
265
      }
UNCOV
266
      if (requestSequenceNumber !== this.requestSequenceNumber) {
1!
267
        return;
×
268
      }
269

UNCOV
270
      this.error = normalizeError(error);
1✔
UNCOV
271
      this.emitError(this.error);
1✔
UNCOV
272
      this.emitUpdate();
1✔
273
    } finally {
UNCOV
274
      if (this.abortController === abortController) {
12✔
UNCOV
275
        this.abortController = null;
10✔
276
      }
UNCOV
277
      this._finishLoading();
12✔
278
    }
279
  }
280

281
  /** Derives generic feature request parameters from the active deck.gl viewport. */
282
  private _getRequestParameters(viewport: Viewport): GetFeaturesParameters {
UNCOV
283
    const bounds = viewport.getBounds();
15✔
284

UNCOV
285
    return {
15✔
286
      layers: this.layers,
287
      boundingBox: [
288
        [bounds[0], bounds[1]],
289
        [bounds[2], bounds[3]]
290
      ],
291
      crs: this.crs,
292
      format: this.format
293
    };
294
  }
295

296
  private emitUpdate(): void {
UNCOV
297
    for (const subscription of this.subscriptions) {
45✔
UNCOV
298
      subscription.onUpdate?.();
28✔
299
    }
300
  }
301

302
  private emitLoadingStateChange(isLoading: boolean): void {
UNCOV
303
    for (const subscription of this.subscriptions) {
20✔
UNCOV
304
      subscription.onLoadingStateChange?.(isLoading);
12✔
305
    }
306
  }
307

308
  private emitDataLoad(table: VectorSetData): void {
UNCOV
309
    for (const subscription of this.subscriptions) {
9✔
UNCOV
310
      subscription.onDataLoad?.(table);
5✔
311
    }
312
  }
313

314
  private emitMetadataLoad(metadata: VectorSourceMetadata): void {
UNCOV
315
    for (const subscription of this.subscriptions) {
3✔
UNCOV
316
      subscription.onMetadataLoad?.(metadata);
3✔
317
    }
318
  }
319

320
  private emitSchemaLoad(schema: Schema): void {
UNCOV
321
    for (const subscription of this.subscriptions) {
3✔
UNCOV
322
      subscription.onSchemaLoad?.(schema);
3✔
323
    }
324
  }
325

326
  private emitError(error: Error): void {
UNCOV
327
    for (const subscription of this.subscriptions) {
1✔
UNCOV
328
      subscription.onError?.(error);
1✔
329
    }
330
  }
331

332
  /** Increments the active load count and emits loading-state transitions. */
333
  private _startLoading(): void {
UNCOV
334
    const wasLoading = this.loadCounter > 0;
15✔
UNCOV
335
    this.loadCounter++;
15✔
UNCOV
336
    this.isLoading = true;
15✔
UNCOV
337
    if (!wasLoading) {
15✔
UNCOV
338
      this.emitLoadingStateChange(true);
10✔
UNCOV
339
      this.emitUpdate();
10✔
340
    }
341
  }
342

343
  /** Decrements the active load count and emits loading-state transitions. */
344
  private _finishLoading(): void {
UNCOV
345
    const wasLoading = this.loadCounter > 0;
15✔
UNCOV
346
    this.loadCounter = Math.max(0, this.loadCounter - 1);
15✔
UNCOV
347
    this.isLoading = this.loadCounter > 0;
15✔
UNCOV
348
    if (wasLoading && !this.isLoading) {
15✔
UNCOV
349
      this.emitLoadingStateChange(false);
10✔
UNCOV
350
      this.emitUpdate();
10✔
351
    }
352
  }
353

354
  /** Clears any pending debounced viewport request before it hits the network. */
355
  private _cancelScheduledRequest(): void {
UNCOV
356
    if (this.timeoutId !== null) {
14✔
UNCOV
357
      clearTimeout(this.timeoutId);
2✔
UNCOV
358
      this.timeoutId = null;
2✔
359
    }
UNCOV
360
    this.pendingTimeoutResolve?.();
14✔
UNCOV
361
    this.pendingTimeoutResolve = null;
14✔
362
  }
363

364
  /** Aborts the current in-flight vector request, if any. */
365
  private _abortActiveRequest(): void {
UNCOV
366
    this.abortController?.abort();
14✔
UNCOV
367
    this.abortController = null;
14✔
368
  }
369
}
370

371
function isAbortError(error: unknown): boolean {
UNCOV
372
  return error instanceof Error && error.name === 'AbortError';
1✔
373
}
374

375
function getRequestKey(parameters: GetFeaturesParameters): string {
UNCOV
376
  const layers = Array.isArray(parameters.layers) ? parameters.layers.join(',') : parameters.layers;
15!
UNCOV
377
  const crs = parameters.crs || '';
15!
UNCOV
378
  const format = parameters.format || '';
15✔
UNCOV
379
  const boundingBox = parameters.boundingBox.flat().join(',');
15✔
UNCOV
380
  return `${layers}|${crs}|${format}|${boundingBox}`;
15✔
381
}
382

383
function areLayerSelectionsEqual(left: string | string[], right: string | string[]): boolean {
UNCOV
384
  if (Array.isArray(left) && Array.isArray(right)) {
3!
UNCOV
385
    return (
3✔
UNCOV
386
      left.length === right.length && left.every((layerName, index) => layerName === right[index])
3✔
387
    );
388
  }
389
  return left === right;
×
390
}
391

392
function normalizeError(error: unknown): Error {
UNCOV
393
  return error instanceof Error ? error : new Error(String(error));
1!
394
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc