@carto/api-client 0.5.0-alpha.5 → 0.5.0-alpha.7

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.
@@ -195,12 +195,12 @@ export class WidgetTilesetSource extends WidgetSource<WidgetTilesetSourceProps>
195
195
  filterOwner
196
196
  );
197
197
 
198
- assertColumn(this._features, column);
199
-
200
198
  if (!this._features.length) {
201
199
  return [];
202
200
  }
203
201
 
202
+ assertColumn(this._features, column);
203
+
204
204
  return histogram({
205
205
  data: filteredFeatures,
206
206
  valuesColumns: normalizeColumns(column),
@@ -24,20 +24,119 @@ import {Method} from '../workers/constants.js';
24
24
  import {WorkerRequest, WorkerResponse} from '../workers/types.js';
25
25
 
26
26
  /**
27
- * TODO
27
+ * Wrapper for {@link WidgetTilesetSource}, moving calculations to Web Workers.
28
+ * When supported, use of both classes is identical.
29
+ *
30
+ * To use this wrapper, the application and environment must support ESM Web
31
+ * Workers. For older build systems based on CommonJS, or in environments like
32
+ * Node.js, it may be necessary to use {@link WidgetTilesetSource} directly,
33
+ * and to (optionally) create workers manually in the application.
28
34
  */
29
35
  export class WidgetTilesetWorkerSource extends WidgetSource<WidgetTilesetSourceProps> {
30
36
  constructor(props: WidgetTilesetSourceProps) {
31
37
  super(props);
38
+ }
39
+
40
+ /////////////////////////////////////////////////////////////////////////////
41
+ // WEB WORKER MANAGEMENT
42
+
43
+ protected _worker: Worker | null = null;
44
+ protected _workerNextRequestId = 1;
45
+
46
+ /**
47
+ * Returns an initialized Worker, to be reused for the lifecycle of this
48
+ * source instance.
49
+ */
50
+ _getWorker() {
51
+ if (this._worker) {
52
+ return this._worker;
53
+ }
32
54
 
33
- WidgetTilesetWorkerSource.init();
34
- WidgetTilesetWorkerSource.WORKER.postMessage({
35
- tableName: this.props.tableName,
55
+ this._worker = new Worker(
56
+ new URL('@carto/api-client/worker', import.meta.url),
57
+ {
58
+ type: 'module',
59
+ name: 'cartowidgettileset',
60
+ }
61
+ );
62
+
63
+ this._worker.postMessage({
36
64
  method: Method.INIT,
37
65
  params: [this.props],
38
66
  } as WorkerRequest);
67
+
68
+ return this._worker;
69
+ }
70
+
71
+ /** Executes a given method on the worker. */
72
+ _executeWorkerMethod<T>(
73
+ method: Method,
74
+ params: unknown[],
75
+ signal?: AbortSignal
76
+ ): Promise<T> {
77
+ const worker = this._getWorker();
78
+ const requestId = this._workerNextRequestId++;
79
+
80
+ // TODO: ViewState may contain non-serializable data, which we do not need.
81
+ // Remove this sanitization after sc-469614 is fixed.
82
+ const options = params[0] as any;
83
+ if (options?.spatialIndexReferenceViewState) {
84
+ const {zoom, latitude, longitude} =
85
+ options.spatialIndexReferenceViewState;
86
+ options.spatialIndexReferenceViewState = {zoom, latitude, longitude};
87
+ }
88
+
89
+ let resolve: ((value: T) => void) | null = null;
90
+ let reject: ((reason: any) => void) | null = null;
91
+
92
+ // If worker sends message to main process, check whether it's a response
93
+ // to this request, and whether the request can been aborted. Then resolve
94
+ // or reject the Promise.
95
+ function onMessage(e: MessageEvent) {
96
+ const response = e.data as WorkerResponse;
97
+ if (response.requestId !== requestId) return;
98
+
99
+ if (signal?.aborted) {
100
+ reject!(new Error(signal.reason));
101
+ } else if (response.ok) {
102
+ resolve!(response.result as T);
103
+ } else {
104
+ reject!(new Error(response.error));
105
+ }
106
+ }
107
+
108
+ // If request is aborted by user, immediately reject the Promise.
109
+ function onAbort() {
110
+ reject!(new Error(signal!.reason));
111
+ }
112
+
113
+ worker.addEventListener('message', onMessage);
114
+ signal?.addEventListener('abort', onAbort);
115
+
116
+ // Send the task to the worker, creating a Promise to resolve/reject later.
117
+ const promise = new Promise<T>((_resolve, _reject) => {
118
+ resolve = _resolve;
119
+ reject = _reject;
120
+
121
+ worker.postMessage({
122
+ requestId,
123
+ method,
124
+ params,
125
+ } as WorkerRequest);
126
+ });
127
+
128
+ // Whether the task completes, fails, or aborts: clean up afterward.
129
+ void promise.finally(() => {
130
+ worker.removeEventListener('message', onMessage);
131
+ signal?.removeEventListener('abort', onAbort);
132
+ });
133
+
134
+ return promise;
39
135
  }
40
136
 
137
+ /////////////////////////////////////////////////////////////////////////////
138
+ // DATA LOADING
139
+
41
140
  /**
42
141
  * Loads features as a list of tiles (typically provided by deck.gl).
43
142
  * After tiles are loaded, {@link extractTileFeatures} must be called
@@ -50,8 +149,7 @@ export class WidgetTilesetWorkerSource extends WidgetSource<WidgetTilesetSourceP
50
149
  data,
51
150
  }));
52
151
 
53
- WidgetTilesetWorkerSource.WORKER.postMessage({
54
- tableName: this.props.tableName,
152
+ this._getWorker().postMessage({
55
153
  method: Method.LOAD_TILES,
56
154
  params: [tiles],
57
155
  } as WorkerRequest);
@@ -59,8 +157,7 @@ export class WidgetTilesetWorkerSource extends WidgetSource<WidgetTilesetSourceP
59
157
 
60
158
  /** Configures options used to extract features from tiles. */
61
159
  setTileFeatureExtractOptions(options: TileFeatureExtractOptions) {
62
- WidgetTilesetWorkerSource.WORKER.postMessage({
63
- tableName: this.props.tableName,
160
+ this._getWorker().postMessage({
64
161
  type: Method.SET_TILE_FEATURE_EXTRACT_OPTIONS,
65
162
  params: [options],
66
163
  });
@@ -78,161 +175,66 @@ export class WidgetTilesetWorkerSource extends WidgetSource<WidgetTilesetSourceP
78
175
  geojson: FeatureCollection;
79
176
  spatialFilter: SpatialFilter;
80
177
  }) {
81
- WidgetTilesetWorkerSource.WORKER.postMessage({
82
- tableName: this.props.tableName,
178
+ this._getWorker().postMessage({
83
179
  method: Method.LOAD_GEOJSON,
84
180
  params: [{geojson, spatialFilter}],
85
181
  } as WorkerRequest);
86
182
  }
87
183
 
184
+ /////////////////////////////////////////////////////////////////////////////
185
+ // WIDGETS API
186
+
88
187
  // eslint-disable-next-line @typescript-eslint/require-await
89
188
  override async getFeatures(): Promise<FeaturesResponse> {
90
189
  throw new Error('getFeatures not supported for tilesets');
91
190
  }
92
191
 
93
192
  async getFormula({
94
- abortController,
193
+ signal,
95
194
  ...options
96
195
  }: FormulaRequestOptions): Promise<FormulaResponse> {
97
- return this._executeWorkerMethod(
98
- this.props.tableName,
99
- Method.GET_FORMULA,
100
- [options],
101
- abortController?.signal
102
- );
196
+ return this._executeWorkerMethod(Method.GET_FORMULA, [options], signal);
103
197
  }
104
198
 
105
199
  override async getHistogram({
106
- abortController,
200
+ signal,
107
201
  ...options
108
202
  }: HistogramRequestOptions): Promise<HistogramResponse> {
109
- return this._executeWorkerMethod(
110
- this.props.tableName,
111
- Method.GET_HISTOGRAM,
112
- [options],
113
- abortController?.signal
114
- );
203
+ return this._executeWorkerMethod(Method.GET_HISTOGRAM, [options], signal);
115
204
  }
116
205
 
117
206
  override async getCategories({
118
- abortController,
207
+ signal,
119
208
  ...options
120
209
  }: CategoryRequestOptions): Promise<CategoryResponse> {
121
- return this._executeWorkerMethod(
122
- this.props.tableName,
123
- Method.GET_CATEGORIES,
124
- [options],
125
- abortController?.signal
126
- );
210
+ return this._executeWorkerMethod(Method.GET_CATEGORIES, [options], signal);
127
211
  }
128
212
 
129
213
  override async getScatter({
130
- abortController,
214
+ signal,
131
215
  ...options
132
216
  }: ScatterRequestOptions): Promise<ScatterResponse> {
133
- return this._executeWorkerMethod(
134
- this.props.tableName,
135
- Method.GET_SCATTER,
136
- [options],
137
- abortController?.signal
138
- );
217
+ return this._executeWorkerMethod(Method.GET_SCATTER, [options], signal);
139
218
  }
140
219
 
141
220
  override async getTable({
142
- abortController,
221
+ signal,
143
222
  ...options
144
223
  }: TableRequestOptions): Promise<TableResponse> {
145
- return this._executeWorkerMethod(
146
- this.props.tableName,
147
- Method.GET_TABLE,
148
- [options],
149
- abortController?.signal
150
- );
224
+ return this._executeWorkerMethod(Method.GET_TABLE, [options], signal);
151
225
  }
152
226
 
153
227
  override async getTimeSeries({
154
- abortController,
228
+ signal,
155
229
  ...options
156
230
  }: TimeSeriesRequestOptions): Promise<TimeSeriesResponse> {
157
- return this._executeWorkerMethod(
158
- this.props.tableName,
159
- Method.GET_TIME_SERIES,
160
- [options],
161
- abortController?.signal
162
- );
231
+ return this._executeWorkerMethod(Method.GET_TIME_SERIES, [options], signal);
163
232
  }
164
233
 
165
234
  override async getRange({
166
- abortController,
235
+ signal,
167
236
  ...options
168
237
  }: RangeRequestOptions): Promise<RangeResponse> {
169
- return this._executeWorkerMethod(
170
- this.props.tableName,
171
- Method.GET_RANGE,
172
- [options],
173
- abortController?.signal
174
- );
175
- }
176
-
177
- /////////////////////////////////////////////////////////////////////////////
178
- // WEB WORKER MANAGEMENT
179
-
180
- // TODO: Singleton? Pool shared among datasets? One per dataset?
181
- protected static WORKER: Worker;
182
- protected static _nextRequestID = 1;
183
-
184
- static init() {
185
- WidgetTilesetWorkerSource.WORKER = new Worker(
186
- new URL('@carto/api-client/worker', import.meta.url),
187
- {
188
- type: 'module',
189
- name: 'cartowidgettileset',
190
- }
191
- );
192
- }
193
-
194
- _executeWorkerMethod<T>(
195
- tableName: string,
196
- method: Method,
197
- params: unknown[],
198
- signal?: AbortSignal
199
- ): Promise<T> {
200
- const worker = WidgetTilesetWorkerSource.WORKER;
201
- const requestId = WidgetTilesetWorkerSource._nextRequestID++;
202
-
203
- // TODO: ViewState may contain non-serializable data, which we do not need.
204
- // Remove this sanitization after sc-469614 is fixed.
205
- const options = params[0] as any;
206
- if (options?.spatialIndexReferenceViewState) {
207
- const {zoom, latitude, longitude} =
208
- options.spatialIndexReferenceViewState;
209
- options.spatialIndexReferenceViewState = {zoom, latitude, longitude};
210
- }
211
-
212
- worker.postMessage({
213
- requestId,
214
- tableName,
215
- method,
216
- params,
217
- } as WorkerRequest);
218
-
219
- return new Promise((resolve, reject) => {
220
- function listener(e: MessageEvent) {
221
- const response = e.data as WorkerResponse;
222
- if (response.requestId !== requestId) return;
223
-
224
- worker.removeEventListener('message', listener);
225
-
226
- if (signal?.aborted) {
227
- reject(new Error(signal.reason));
228
- } else if (response.ok) {
229
- resolve(response.result as T);
230
- } else {
231
- reject(new Error(response.error));
232
- }
233
- }
234
-
235
- worker.addEventListener('message', listener);
236
- });
238
+ return this._executeWorkerMethod(Method.GET_RANGE, [options], signal);
237
239
  }
238
240
  }
@@ -2,7 +2,6 @@ import type {Method} from './constants.js';
2
2
 
3
3
  export type WorkerRequest = {
4
4
  requestId?: number;
5
- tableName: string; // TODO: Table name is not a unique identifier.
6
5
  method: Method;
7
6
  params: unknown[];
8
7
  };
@@ -5,22 +5,25 @@ import {
5
5
  import {Method} from './constants.js';
6
6
  import type {WorkerRequest, WorkerResponse} from './types.js';
7
7
 
8
- // TODO: Cannot rely on tableName as unique ID.
9
- const SOURCES_BY_NAME = new Map<string, WidgetTilesetSource>();
8
+ /*
9
+ * Web Worker, compiled as a separate `@carto/api-client/worker` entrypoint.
10
+ *
11
+ * Workers are scoped to the lifecycle of a single WidgetTilesetWorkerSource
12
+ * instance, representing and executing calculations on a single datasource.
13
+ */
14
+
15
+ let source: WidgetTilesetSource;
10
16
 
11
17
  addEventListener('message', (e) => {
12
- const {tableName, method, params, requestId} = e.data as WorkerRequest;
18
+ const {method, params, requestId} = e.data as WorkerRequest;
13
19
 
14
20
  if (method === Method.INIT) {
15
- const props = params[0] as WidgetTilesetSourceProps;
16
- SOURCES_BY_NAME.set(tableName, new WidgetTilesetSource(props));
21
+ source = new WidgetTilesetSource(params[0] as WidgetTilesetSourceProps);
17
22
  return;
18
23
  }
19
24
 
20
- const source = SOURCES_BY_NAME.get(tableName);
21
-
22
25
  if (!source) {
23
- const error = `Unknown dataset: ${tableName}`;
26
+ const error = `Cannot execute "${method}" on uninitialized source.`;
24
27
  postMessage({ok: false, error, requestId} as WorkerResponse);
25
28
  return;
26
29
  }
@@ -1829,10 +1829,10 @@ var WidgetTilesetSource = class extends WidgetSource {
1829
1829
  filters,
1830
1830
  filterOwner
1831
1831
  );
1832
- assertColumn(this._features, column);
1833
1832
  if (!this._features.length) {
1834
1833
  return [];
1835
1834
  }
1835
+ assertColumn(this._features, column);
1836
1836
  return histogram({
1837
1837
  data: filteredFeatures,
1838
1838
  valuesColumns: normalizeColumns(column),