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

visgl / loaders.gl / 25798210458

13 May 2026 12:10PM UTC coverage: 60.27%. Remained the same
25798210458

push

github

web-flow
Remove caution about LAS v1.4 file support (#3272)

13159 of 24150 branches covered (54.49%)

Branch coverage included in aggregate %.

27149 of 42729 relevant lines covered (63.54%)

15182.84 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. */
80
  isLoaded = false;
9✔
81
  /** True while the latest viewport request is in flight. */
82
  isLoading = false;
9✔
83
  /** Latest accepted viewport table. */
84
  data: VectorSetData | null = null;
9✔
85
  /** Resolved source schema. */
86
  schema: Schema | null = null;
9✔
87
  /** Resolved source metadata. */
88
  metadata: VectorSourceMetadata | null = null;
9✔
89
  /** Latest accepted error. */
90
  error: Error | null = null;
9✔
91

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

101
  /** Creates a new viewport-driven vector runtime for a source. */
102
  constructor(options: VectorSetOptions) {
103
    this.vectorSource = options.vectorSource;
9✔
104
    this.layers = options.layers;
9✔
105
    this.crs = options.crs;
9✔
106
    this.format = options.format;
9✔
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
  ) {
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 {
132
    const sourceChanged = options.vectorSource !== this.vectorSource;
3✔
133
    const layersChanged = !areLayerSelectionsEqual(options.layers, this.layers);
3✔
134
    const crsChanged = options.crs !== this.crs;
3✔
135
    const formatChanged = options.format !== this.format;
3✔
136

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

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

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> {
166
    this.metadataPromise ||= this._loadMetadata();
3✔
167
    await this.metadataPromise;
3✔
168
  }
169

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

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

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

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

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

197
  /** Subscribes to runtime state changes. */
198
  subscribe(events: VectorSetEvents): () => void {
199
    this.subscriptions.add(events);
5✔
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> {
212
    this._startLoading();
3✔
213
    try {
3✔
214
      const [metadata, schema] = await Promise.all([
3✔
215
        this.vectorSource.getMetadata({formatSpecificMetadata: false}),
216
        this.vectorSource.getSchema()
217
      ]);
218

219
      this.metadata = metadata;
3✔
220
      this.schema = schema;
3✔
221
      this.emitMetadataLoad(metadata);
3✔
222
      this.emitSchemaLoad(schema);
3✔
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 {
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> {
239
    this.lastRequestKey = requestKey;
12✔
240
    const requestSequenceNumber = ++this.requestSequenceNumber;
12✔
241
    const abortController = new AbortController();
12✔
242

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

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

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

270
      this.error = normalizeError(error);
1✔
271
      this.emitError(this.error);
1✔
272
      this.emitUpdate();
1✔
273
    } finally {
274
      if (this.abortController === abortController) {
12✔
275
        this.abortController = null;
10✔
276
      }
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 {
283
    const bounds = viewport.getBounds();
15✔
284

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 {
297
    for (const subscription of this.subscriptions) {
45✔
298
      subscription.onUpdate?.();
28✔
299
    }
300
  }
301

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

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

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

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

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

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

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

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

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

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

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

383
function areLayerSelectionsEqual(left: string | string[], right: string | string[]): boolean {
384
  if (Array.isArray(left) && Array.isArray(right)) {
3!
385
    return (
3✔
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 {
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