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

visgl / loaders.gl / 24310065355

12 Apr 2026 03:30PM UTC coverage: 55.777% (+0.3%) from 55.485%
24310065355

push

github

web-flow
feat: new VectorSource (#3373)

9401 of 18236 branches covered (51.55%)

Branch coverage included in aggregate %.

226 of 276 new or added lines in 9 files covered. (81.88%)

4 existing lines in 1 file now uncovered.

19600 of 33759 relevant lines covered (58.06%)

5031.76 hits per line

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

81.05
/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 {BinaryFeatureCollection, GeoJSONTable, Schema} from '@loaders.gl/schema';
7
import type {GetFeaturesParameters, VectorSource, VectorSourceMetadata} from '@loaders.gl/loader-utils';
8

9
/** Mutable fetch options consumed by {@link VectorSet}. */
10
export type VectorSetOptions = {
11
  /** Source used to resolve viewport feature requests. */
12
  vectorSource: VectorSource;
13
  /** Named source layers included in each request. */
14
  layers: string | string[];
15
  /** Output CRS forwarded to the vector source. */
16
  crs?: string;
17
  /** Debounce interval applied before issuing viewport requests. */
18
  debounceTime?: number;
19
};
20

21
/** Readable snapshot of the current vector-set state. */
22
export type VectorSetState = {
23
  /** True after metadata/schema resolve successfully and a current table is available. */
24
  isLoaded: boolean;
25
  /** True while the latest viewport request is in flight. */
26
  isLoading: boolean;
27
  /** Latest accepted viewport table. */
28
  data: GeoJSONTable | BinaryFeatureCollection | null;
29
  /** Resolved source schema when available. */
30
  schema: Schema | null;
31
  /** Resolved source metadata when available. */
32
  metadata: VectorSourceMetadata | null;
33
  /** Latest accepted error, if any. */
34
  error: Error | null;
35
};
36

37
/** Subscription callbacks emitted by {@link VectorSet}. */
38
export type VectorSetEvents = {
39
  /** Called when metadata/viewport loading starts or stops. */
40
  onLoadingStateChange?: (isLoading: boolean) => void;
41
  /** Called whenever any public state changes. */
42
  onUpdate?: () => void;
43
  /** Called when a viewport request resolves and becomes current. */
44
  onDataLoad?: (table: GeoJSONTable | BinaryFeatureCollection) => void;
45
  /** Called when metadata resolves. */
46
  onMetadataLoad?: (metadata: VectorSourceMetadata) => void;
47
  /** Called when schema resolves. */
48
  onSchemaLoad?: (schema: Schema) => void;
49
  /** Called when the current metadata or viewport request fails. */
50
  onError?: (error: Error) => void;
51
};
52

53
/**
54
 * Small runtime helper that keeps the latest vector table in sync with the active viewport.
55
 *
56
 * `VectorSet` only accepts the most recent viewport request result and ignores stale responses.
57
 */
58
export class VectorSet {
59
  /** Current source and request options. */
60
  vectorSource: VectorSource;
61
  /** Layers forwarded to `VectorSource#getFeatures`. */
62
  layers: string | string[];
63
  /** Output CRS forwarded to `VectorSource#getFeatures`. */
64
  crs?: string;
65
  /** Debounce interval applied before issuing viewport requests. */
66
  debounceTime: number;
67

68
  /** True once the latest accepted viewport request completes successfully. */
69
  isLoaded = false;
6✔
70
  /** True while the latest viewport request is in flight. */
71
  isLoading = false;
6✔
72
  /** Latest accepted viewport table. */
73
  data: GeoJSONTable | BinaryFeatureCollection | null = null;
6✔
74
  /** Resolved source schema. */
75
  schema: Schema | null = null;
6✔
76
  /** Resolved source metadata. */
77
  metadata: VectorSourceMetadata | null = null;
6✔
78
  /** Latest accepted error. */
79
  error: Error | null = null;
6✔
80

81
  private readonly subscriptions = new Set<VectorSetEvents>();
6✔
82
  private metadataPromise: Promise<void> | null = null;
6✔
83
  private requestSequenceNumber = 0;
6✔
84
  private lastRequestKey: string | null = null;
6✔
85
  private loadCounter = 0;
6✔
86
  private timeoutId: ReturnType<typeof setTimeout> | null = null;
6✔
87
  private pendingTimeoutResolve: (() => void) | null = null;
6✔
88
  private abortController: AbortController | null = null;
6✔
89

90
  /** Creates a new viewport-driven vector runtime for a source. */
91
  constructor(options: VectorSetOptions) {
92
    this.vectorSource = options.vectorSource;
6✔
93
    this.layers = options.layers;
6✔
94
    this.crs = options.crs;
6✔
95
    this.debounceTime = options.debounceTime ?? 200;
6!
96
  }
97

98
  /** Creates a `VectorSet` from a vector source instance. */
99
  static fromVectorSource(vectorSource: VectorSource, options: Omit<VectorSetOptions, 'vectorSource'>) {
100
    return new VectorSet({vectorSource, ...options});
2✔
101
  }
102

103
  /** Returns a readable snapshot for tests and consumers. */
104
  getState(): VectorSetState {
NEW
105
    return {
×
106
      isLoaded: this.isLoaded,
107
      isLoading: this.isLoading,
108
      data: this.data,
109
      schema: this.schema,
110
      metadata: this.metadata,
111
      error: this.error
112
    };
113
  }
114

115
  /** Updates the source or request options used for subsequent viewport fetches. */
116
  setOptions(options: VectorSetOptions): void {
117
    const sourceChanged = options.vectorSource !== this.vectorSource;
2✔
118
    const layersChanged = !areLayerSelectionsEqual(options.layers, this.layers);
2✔
119
    const crsChanged = options.crs !== this.crs;
2✔
120

121
    this.vectorSource = options.vectorSource;
2✔
122
    this.layers = options.layers;
2✔
123
    this.crs = options.crs;
2✔
124
    this.debounceTime = options.debounceTime ?? this.debounceTime;
2!
125

126
    if (sourceChanged) {
2!
NEW
127
      this._cancelScheduledRequest();
×
NEW
128
      this.metadataPromise = null;
×
NEW
129
      this.schema = null;
×
NEW
130
      this.metadata = null;
×
NEW
131
      this.data = null;
×
NEW
132
      this.error = null;
×
NEW
133
      this.isLoaded = false;
×
NEW
134
      this.isLoading = false;
×
NEW
135
      this.loadCounter = 0;
×
NEW
136
      this.lastRequestKey = null;
×
NEW
137
      this.requestSequenceNumber++;
×
NEW
138
      this.emitUpdate();
×
NEW
139
      return;
×
140
    }
141

142
    if (layersChanged || crsChanged) {
2!
NEW
143
      this.lastRequestKey = null;
×
144
    }
145
  }
146

147
  /** Loads metadata and schema once for the current source. */
148
  async loadMetadata(): Promise<void> {
149
    this.metadataPromise ||= this._loadMetadata();
2✔
150
    await this.metadataPromise;
2✔
151
  }
152

153
  /** Requests features for the supplied viewport when inputs changed. */
154
  async updateViewport(viewport: Viewport): Promise<void> {
155
    const requestParameters = this._getRequestParameters(viewport);
11✔
156
    const requestKey = getRequestKey(requestParameters);
11✔
157

158
    if (requestKey === this.lastRequestKey) {
11✔
159
      return;
1✔
160
    }
161

162
    this._cancelScheduledRequest();
10✔
163
    this._abortActiveRequest();
10✔
164

165
    if (this.debounceTime > 0) {
10✔
166
      await new Promise<void>(resolve => {
4✔
167
        this.pendingTimeoutResolve = resolve;
4✔
168
        this.timeoutId = setTimeout(() => {
4✔
169
          this.timeoutId = null;
2✔
170
          this.pendingTimeoutResolve = null;
2✔
171
          void this._loadFeatures(requestParameters, requestKey).finally(resolve);
2✔
172
        }, this.debounceTime);
173
      });
174
      return;
4✔
175
    }
176

177
    await this._loadFeatures(requestParameters, requestKey);
6✔
178
  }
179

180
  /** Subscribes to runtime state changes. */
181
  subscribe(events: VectorSetEvents): () => void {
182
    this.subscriptions.add(events);
3✔
183
    return () => this.subscriptions.delete(events);
3✔
184
  }
185

186
  /** Releases references and cancels acceptance of any in-flight request. */
187
  finalize(): void {
NEW
188
    this.requestSequenceNumber++;
×
NEW
189
    this._cancelScheduledRequest();
×
NEW
190
    this._abortActiveRequest();
×
NEW
191
    this.subscriptions.clear();
×
192
  }
193

194
  private async _loadMetadata(): Promise<void> {
195
    this._startLoading();
2✔
196
    try {
2✔
197
      const [metadata, schema] = await Promise.all([
2✔
198
        this.vectorSource.getMetadata({formatSpecificMetadata: false}),
199
        this.vectorSource.getSchema()
200
      ]);
201

202
      this.metadata = metadata;
2✔
203
      this.schema = schema;
2✔
204
      this.emitMetadataLoad(metadata);
2✔
205
      this.emitSchemaLoad(schema);
2✔
206
      this.emitUpdate();
2✔
207
    } catch (error) {
NEW
208
      this.error = normalizeError(error);
×
NEW
209
      this.emitError(this.error);
×
NEW
210
      this.emitUpdate();
×
NEW
211
      throw this.error;
×
212
    } finally {
213
      this._finishLoading();
2✔
214
    }
215
  }
216

217
  /** Issues the actual vector source request after debounce completes. */
218
  private async _loadFeatures(
219
    requestParameters: GetFeaturesParameters,
220
    requestKey: string
221
  ): Promise<void> {
222
    this.lastRequestKey = requestKey;
8✔
223
    const requestSequenceNumber = ++this.requestSequenceNumber;
8✔
224

225
    this._startLoading();
8✔
226
    this.error = null;
8✔
227
    this.emitUpdate();
8✔
228

229
    try {
8✔
230
      const abortController = new AbortController();
8✔
231
      this.abortController = abortController;
8✔
232
      const table = await this.vectorSource.getFeatures({
8✔
233
        ...requestParameters,
234
        signal: abortController.signal
235
      });
236
      if (requestSequenceNumber !== this.requestSequenceNumber) {
7✔
237
        return;
1✔
238
      }
239

240
      this.data = table;
6✔
241
      this.error = null;
6✔
242
      this.isLoaded = true;
6✔
243
      this.emitDataLoad(table);
6✔
244
      this.emitUpdate();
6✔
245
    } catch (error) {
246
      if (isAbortError(error)) {
1!
NEW
247
        return;
×
248
      }
249
      if (requestSequenceNumber !== this.requestSequenceNumber) {
1!
NEW
250
        return;
×
251
      }
252

253
      this.error = normalizeError(error);
1✔
254
      this.emitError(this.error);
1✔
255
      this.emitUpdate();
1✔
256
    } finally {
257
      if (requestSequenceNumber === this.requestSequenceNumber) {
8✔
258
        this._finishLoading();
7✔
259
      }
260
    }
261
  }
262

263
  /** Derives generic feature request parameters from the active deck.gl viewport. */
264
  private _getRequestParameters(viewport: Viewport): GetFeaturesParameters {
265
    const bounds = viewport.getBounds();
11✔
266

267
    return {
11✔
268
      layers: this.layers,
269
      boundingBox: [
270
        [bounds[0], bounds[1]],
271
        [bounds[2], bounds[3]]
272
      ],
273
      crs: this.crs
274
    };
275
  }
276

277
  private emitUpdate(): void {
278
    for (const subscription of this.subscriptions) {
30✔
279
      subscription.onUpdate?.();
18✔
280
    }
281
  }
282

283
  private emitLoadingStateChange(isLoading: boolean): void {
284
    for (const subscription of this.subscriptions) {
13✔
285
      subscription.onLoadingStateChange?.(isLoading);
8✔
286
    }
287
  }
288

289
  private emitDataLoad(table: GeoJSONTable | BinaryFeatureCollection): void {
290
    for (const subscription of this.subscriptions) {
6✔
291
      subscription.onDataLoad?.(table);
3✔
292
    }
293
  }
294

295
  private emitMetadataLoad(metadata: VectorSourceMetadata): void {
296
    for (const subscription of this.subscriptions) {
2✔
297
      subscription.onMetadataLoad?.(metadata);
2✔
298
    }
299
  }
300

301
  private emitSchemaLoad(schema: Schema): void {
302
    for (const subscription of this.subscriptions) {
2✔
303
      subscription.onSchemaLoad?.(schema);
2✔
304
    }
305
  }
306

307
  private emitError(error: Error): void {
308
    for (const subscription of this.subscriptions) {
1✔
309
      subscription.onError?.(error);
1✔
310
    }
311
  }
312

313
  /** Increments the active load count and emits loading-state transitions. */
314
  private _startLoading(): void {
315
    const wasLoading = this.loadCounter > 0;
10✔
316
    this.loadCounter++;
10✔
317
    this.isLoading = true;
10✔
318
    if (!wasLoading) {
10✔
319
      this.emitLoadingStateChange(true);
7✔
320
      this.emitUpdate();
7✔
321
    }
322
  }
323

324
  /** Decrements the active load count and emits loading-state transitions. */
325
  private _finishLoading(): void {
326
    const wasLoading = this.loadCounter > 0;
9✔
327
    this.loadCounter = Math.max(0, this.loadCounter - 1);
9✔
328
    this.isLoading = this.loadCounter > 0;
9✔
329
    if (wasLoading && !this.isLoading) {
9✔
330
      this.emitLoadingStateChange(false);
6✔
331
      this.emitUpdate();
6✔
332
    }
333
  }
334

335
  /** Clears any pending debounced viewport request before it hits the network. */
336
  private _cancelScheduledRequest(): void {
337
    if (this.timeoutId !== null) {
10✔
338
      clearTimeout(this.timeoutId);
2✔
339
      this.timeoutId = null;
2✔
340
    }
341
    this.pendingTimeoutResolve?.();
10✔
342
    this.pendingTimeoutResolve = null;
10✔
343
  }
344

345
  /** Aborts the current in-flight vector request, if any. */
346
  private _abortActiveRequest(): void {
347
    this.abortController?.abort();
10✔
348
    this.abortController = null;
10✔
349
  }
350
}
351

352
function isAbortError(error: unknown): boolean {
353
  return error instanceof Error && error.name === 'AbortError';
1✔
354
}
355

356
function getRequestKey(parameters: GetFeaturesParameters): string {
357
  const layers = Array.isArray(parameters.layers) ? parameters.layers.join(',') : parameters.layers;
11!
358
  const crs = parameters.crs || '';
11!
359
  const boundingBox = parameters.boundingBox.flat().join(',');
11✔
360
  return `${layers}|${crs}|${boundingBox}`;
11✔
361
}
362

363
function areLayerSelectionsEqual(left: string | string[], right: string | string[]): boolean {
364
  if (Array.isArray(left) && Array.isArray(right)) {
2!
365
    return left.length === right.length && left.every((layerName, index) => layerName === right[index]);
2✔
366
  }
NEW
367
  return left === right;
×
368
}
369

370
function normalizeError(error: unknown): Error {
371
  return error instanceof Error ? error : new Error(String(error));
1!
372
}
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