@loaders.gl/core 4.5.3 → 5.0.0-alpha.10

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.
Files changed (127) hide show
  1. package/LICENSE +14 -3
  2. package/dist/dist.dev.js +1789 -352
  3. package/dist/dist.min.js +4 -3
  4. package/dist/index.cjs +3087 -311
  5. package/dist/index.cjs.map +3 -4
  6. package/dist/index.d.ts +8 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +7 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/iterators/make-iterator/make-stream-iterator.js +48 -9
  11. package/dist/iterators/make-iterator/make-stream-iterator.js.map +1 -1
  12. package/dist/lib/api/core-api.d.ts +6 -0
  13. package/dist/lib/api/core-api.d.ts.map +1 -0
  14. package/dist/lib/api/core-api.js +23 -0
  15. package/dist/lib/api/core-api.js.map +1 -0
  16. package/dist/lib/api/create-data-source.d.ts +2 -2
  17. package/dist/lib/api/create-data-source.d.ts.map +1 -1
  18. package/dist/lib/api/create-data-source.js +36 -14
  19. package/dist/lib/api/create-data-source.js.map +1 -1
  20. package/dist/lib/api/load-in-batches.d.ts +4 -4
  21. package/dist/lib/api/load-in-batches.d.ts.map +1 -1
  22. package/dist/lib/api/load-in-batches.js +17 -6
  23. package/dist/lib/api/load-in-batches.js.map +1 -1
  24. package/dist/lib/api/load-loader.d.ts +10 -0
  25. package/dist/lib/api/load-loader.d.ts.map +1 -0
  26. package/dist/lib/api/load-loader.js +222 -0
  27. package/dist/lib/api/load-loader.js.map +1 -0
  28. package/dist/lib/api/load.d.ts +3 -3
  29. package/dist/lib/api/load.d.ts.map +1 -1
  30. package/dist/lib/api/load.js +84 -5
  31. package/dist/lib/api/load.js.map +1 -1
  32. package/dist/lib/api/parse-file.d.ts +15 -0
  33. package/dist/lib/api/parse-file.d.ts.map +1 -0
  34. package/dist/lib/api/parse-file.js +74 -0
  35. package/dist/lib/api/parse-file.js.map +1 -0
  36. package/dist/lib/api/parse-in-batches.d.ts +2 -2
  37. package/dist/lib/api/parse-in-batches.d.ts.map +1 -1
  38. package/dist/lib/api/parse-in-batches.js +18 -7
  39. package/dist/lib/api/parse-in-batches.js.map +1 -1
  40. package/dist/lib/api/parse-sync.d.ts +2 -2
  41. package/dist/lib/api/parse-sync.d.ts.map +1 -1
  42. package/dist/lib/api/parse-sync.js +10 -1
  43. package/dist/lib/api/parse-sync.js.map +1 -1
  44. package/dist/lib/api/parse.d.ts +2 -2
  45. package/dist/lib/api/parse.d.ts.map +1 -1
  46. package/dist/lib/api/parse.js +34 -17
  47. package/dist/lib/api/parse.js.map +1 -1
  48. package/dist/lib/api/preload.d.ts +14 -0
  49. package/dist/lib/api/preload.d.ts.map +1 -0
  50. package/dist/lib/api/preload.js +13 -0
  51. package/dist/lib/api/preload.js.map +1 -0
  52. package/dist/lib/api/register-loaders.js +1 -1
  53. package/dist/lib/api/register-loaders.js.map +1 -1
  54. package/dist/lib/api/select-loader.d.ts.map +1 -1
  55. package/dist/lib/api/select-loader.js +48 -5
  56. package/dist/lib/api/select-loader.js.map +1 -1
  57. package/dist/lib/api/select-source.d.ts +3 -3
  58. package/dist/lib/api/select-source.d.ts.map +1 -1
  59. package/dist/lib/api/select-source.js +2 -5
  60. package/dist/lib/api/select-source.js.map +1 -1
  61. package/dist/lib/fetch/fetch-error-message.js +1 -1
  62. package/dist/lib/fetch/fetch-error-message.js.map +1 -1
  63. package/dist/lib/filesystems/browser-filesystem.d.ts.map +1 -1
  64. package/dist/lib/filesystems/browser-filesystem.js +25 -11
  65. package/dist/lib/filesystems/browser-filesystem.js.map +1 -1
  66. package/dist/lib/init.js +1 -1
  67. package/dist/lib/init.js.map +1 -1
  68. package/dist/lib/loader-utils/check-errors.js +1 -1
  69. package/dist/lib/loader-utils/check-errors.js.map +1 -1
  70. package/dist/lib/loader-utils/get-data.d.ts +4 -0
  71. package/dist/lib/loader-utils/get-data.d.ts.map +1 -1
  72. package/dist/lib/loader-utils/get-data.js +26 -0
  73. package/dist/lib/loader-utils/get-data.js.map +1 -1
  74. package/dist/lib/loader-utils/get-fetch-function.d.ts +1 -1
  75. package/dist/lib/loader-utils/get-fetch-function.d.ts.map +1 -1
  76. package/dist/lib/loader-utils/get-fetch-function.js +16 -10
  77. package/dist/lib/loader-utils/get-fetch-function.js.map +1 -1
  78. package/dist/lib/loader-utils/loader-context.d.ts +2 -2
  79. package/dist/lib/loader-utils/loader-context.d.ts.map +1 -1
  80. package/dist/lib/loader-utils/loader-context.js +11 -4
  81. package/dist/lib/loader-utils/loader-context.js.map +1 -1
  82. package/dist/lib/loader-utils/loggers.d.ts +19 -0
  83. package/dist/lib/loader-utils/loggers.d.ts.map +1 -1
  84. package/dist/lib/loader-utils/loggers.js +28 -2
  85. package/dist/lib/loader-utils/loggers.js.map +1 -1
  86. package/dist/lib/loader-utils/option-defaults.d.ts +1 -0
  87. package/dist/lib/loader-utils/option-defaults.d.ts.map +1 -1
  88. package/dist/lib/loader-utils/option-defaults.js +5 -0
  89. package/dist/lib/loader-utils/option-defaults.js.map +1 -1
  90. package/dist/lib/loader-utils/option-utils.d.ts +7 -0
  91. package/dist/lib/loader-utils/option-utils.d.ts.map +1 -1
  92. package/dist/lib/loader-utils/option-utils.js +91 -3
  93. package/dist/lib/loader-utils/option-utils.js.map +1 -1
  94. package/dist/lib/utils/response-utils.js +5 -14
  95. package/dist/lib/utils/response-utils.js.map +1 -1
  96. package/dist/null-loader.d.ts.map +1 -1
  97. package/dist/null-loader.js +1 -1
  98. package/dist/null-loader.js.map +1 -1
  99. package/dist/null-worker-node.js +270 -42
  100. package/dist/null-worker.js +270 -42
  101. package/package.json +6 -6
  102. package/src/index.ts +10 -1
  103. package/src/iterators/make-iterator/make-stream-iterator.ts +47 -9
  104. package/src/lib/api/core-api.ts +25 -0
  105. package/src/lib/api/create-data-source.ts +53 -24
  106. package/src/lib/api/load-in-batches.ts +33 -21
  107. package/src/lib/api/load-loader.ts +306 -0
  108. package/src/lib/api/load.ts +160 -9
  109. package/src/lib/api/parse-file.ts +156 -0
  110. package/src/lib/api/parse-in-batches.ts +35 -8
  111. package/src/lib/api/parse-sync.ts +27 -2
  112. package/src/lib/api/parse.ts +61 -20
  113. package/src/lib/api/preload.ts +46 -0
  114. package/src/lib/api/register-loaders.ts +1 -1
  115. package/src/lib/api/select-loader.ts +57 -5
  116. package/src/lib/api/select-source.ts +7 -10
  117. package/src/lib/fetch/fetch-error-message.ts +1 -1
  118. package/src/lib/filesystems/browser-filesystem.ts +25 -11
  119. package/src/lib/loader-utils/check-errors.ts +1 -1
  120. package/src/lib/loader-utils/get-data.ts +36 -0
  121. package/src/lib/loader-utils/get-fetch-function.ts +17 -15
  122. package/src/lib/loader-utils/loader-context.ts +13 -5
  123. package/src/lib/loader-utils/loggers.ts +28 -2
  124. package/src/lib/loader-utils/option-defaults.ts +5 -0
  125. package/src/lib/loader-utils/option-utils.ts +106 -3
  126. package/src/lib/utils/response-utils.ts +5 -15
  127. package/src/null-loader.ts +3 -3
@@ -2,26 +2,23 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import type {Source} from '@loaders.gl/loader-utils';
5
+ import type {SourceLoader} from '@loaders.gl/loader-utils';
6
+ import {selectLoaderSync} from './select-loader';
6
7
 
7
8
  /** Guess service type from URL */
8
9
  export function selectSource(
9
10
  url: string | Blob,
10
- sources: Source[],
11
+ sources: SourceLoader[],
11
12
  options?: {
12
13
  /** Provide id of a source to select that source. Omit or provide 'auto' to test the source*/
13
14
  type?: string;
14
15
  nothrow?: boolean;
15
16
  }
16
- ): Source | null {
17
+ ): SourceLoader | null {
17
18
  const type = options?.type || 'auto';
18
- let selectedSource: Source | null = null;
19
+ let selectedSource: SourceLoader | null = null;
19
20
  if (type === 'auto') {
20
- for (const source of sources) {
21
- if (typeof url === 'string' && source.testURL && source.testURL(url)) {
22
- return source;
23
- }
24
- }
21
+ selectedSource = selectLoaderSync(url, sources, {core: {nothrow: true}}) as SourceLoader | null;
25
22
  } else {
26
23
  selectedSource = getSourceOfType(type, sources);
27
24
  }
@@ -34,7 +31,7 @@ export function selectSource(
34
31
  }
35
32
 
36
33
  /** Guess service type from URL */
37
- function getSourceOfType(type: string, sources: Source[]): Source | null {
34
+ function getSourceOfType(type: string, sources: SourceLoader[]): SourceLoader | null {
38
35
  for (const service of sources) {
39
36
  if (service.type === type) {
40
37
  return service;
@@ -17,7 +17,7 @@ export async function getErrorMessageFromResponse(response: Response): Promise<s
17
17
  } else {
18
18
  message += response.statusText;
19
19
  }
20
- } catch (error) {
20
+ } catch (_error) {
21
21
  // eslint forbids return in finally statement
22
22
  return message;
23
23
  }
@@ -50,23 +50,34 @@ export class BrowserFileSystem implements FileSystem {
50
50
  }
51
51
 
52
52
  // Local fetches are served from the list of files
53
- const file = this.files[path];
53
+ const file = this._getFile(path, true);
54
54
  if (!file) {
55
55
  return new Response(path, {status: 400, statusText: 'NOT FOUND'});
56
56
  }
57
57
 
58
58
  const headers = new Headers(options?.headers);
59
59
  const range = headers.get('Range');
60
- const bytes = range && /bytes=($1)-($2)/.exec(range);
60
+ const bytes = range && /bytes=(\d+)-(\d+)/.exec(range);
61
61
 
62
62
  if (bytes) {
63
63
  const start = parseInt(bytes[1]);
64
- const end = parseInt(bytes[2]);
64
+ const end = Math.min(parseInt(bytes[2]), file.size - 1);
65
+ if (start > end) {
66
+ const response = new Response(null, {
67
+ status: 416,
68
+ headers: {'Content-Range': `bytes */${file.size}`}
69
+ });
70
+ Object.defineProperty(response, 'url', {value: path});
71
+ return response;
72
+ }
65
73
  // The trick when reading File objects is to read successive "slices" of the File
66
74
  // Per spec https://w3c.github.io/FileAPI/, slicing a File should only update the start and end fields
67
75
  // Actually reading from file should happen in `readAsArrayBuffer` (and as far we can tell it does)
68
- const data = await file.slice(start, end).arrayBuffer();
69
- const response = new Response(data);
76
+ const data = await file.slice(start, end + 1).arrayBuffer();
77
+ const response = new Response(data, {
78
+ status: 206,
79
+ headers: {'Content-Range': `bytes ${start}-${end}/${file.size}`}
80
+ });
70
81
  Object.defineProperty(response, 'url', {value: path});
71
82
  return response;
72
83
  }
@@ -95,7 +106,7 @@ export class BrowserFileSystem implements FileSystem {
95
106
  * Return information (size) about files in this file system
96
107
  */
97
108
  async stat(path: string, options?: object): Promise<{size: number}> {
98
- const file = this.files[path];
109
+ const file = this._getFile(path, false);
99
110
  if (!file) {
100
111
  throw new Error(path);
101
112
  }
@@ -106,16 +117,19 @@ export class BrowserFileSystem implements FileSystem {
106
117
  * Just removes the file from the list
107
118
  */
108
119
  async unlink(path: string): Promise<void> {
109
- delete this.files[path];
110
- delete this.lowerCaseFiles[path];
111
- this.usedFiles[path] = true;
120
+ const file = this._getFile(path, false);
121
+ if (file) {
122
+ delete this.files[file.name];
123
+ delete this.lowerCaseFiles[file.name.toLowerCase()];
124
+ this.usedFiles[file.name] = true;
125
+ }
112
126
  }
113
127
 
114
128
  // implements IRandomAccessFileSystem
115
129
 
116
130
  // RANDOM ACCESS
117
131
  async openReadableFile(pathname: string, flags: unknown): Promise<ReadableFile> {
118
- return new BlobFile(this.files[pathname]);
132
+ return new BlobFile(this._getFile(pathname, true));
119
133
  }
120
134
 
121
135
  // PRIVATE
@@ -123,7 +137,7 @@ export class BrowserFileSystem implements FileSystem {
123
137
  // Supports case independent paths, and file usage tracking
124
138
  _getFile(path: string, used: boolean): File {
125
139
  // Prefer case match, but fall back to case independent.
126
- const file = this.files[path] || this.lowerCaseFiles[path];
140
+ const file = this.files[path] || this.lowerCaseFiles[path.toLowerCase()];
127
141
  if (file && used) {
128
142
  this.usedFiles[path] = true;
129
143
  }
@@ -14,7 +14,7 @@ export async function checkFetchResponseStatus(response: Response): Promise<void
14
14
  if (text) {
15
15
  errorMessage += `: ${getErrorText(text)}`;
16
16
  }
17
- } catch (error) {
17
+ } catch (_error) {
18
18
  // ignore error
19
19
  }
20
20
  throw new Error(errorMessage);
@@ -95,6 +95,42 @@ export async function getArrayBufferOrStringFromData(
95
95
  throw new Error(ERR_DATA);
96
96
  }
97
97
 
98
+ /**
99
+ * Resolves the provided data into an {@link ArrayBuffer}, preserving bytes for worker transfer.
100
+ */
101
+ export async function getArrayBufferFromData(
102
+ data: DataType,
103
+ options: LoaderOptions
104
+ ): Promise<ArrayBuffer> {
105
+ if (typeof data === 'string') {
106
+ return new TextEncoder().encode(data).buffer;
107
+ }
108
+
109
+ if (isArrayBufferLike(data)) {
110
+ return toArrayBuffer(toArrayBufferView(data));
111
+ }
112
+
113
+ if (isBlob(data)) {
114
+ data = await makeResponse(data);
115
+ }
116
+
117
+ if (isResponse(data)) {
118
+ await checkResponse(data);
119
+ return await data.arrayBuffer();
120
+ }
121
+
122
+ if (isReadableStream(data)) {
123
+ // @ts-expect-error TS2559 options type
124
+ data = makeIterator(data as ReadableStream, options);
125
+ }
126
+
127
+ if (isIterable(data) || isAsyncIterable(data)) {
128
+ return concatenateArrayBuffersAsync(data as AsyncIterable<ArrayBufferLike>);
129
+ }
130
+
131
+ throw new Error(ERR_DATA);
132
+ }
133
+
98
134
  /**
99
135
  * Normalizes batchable inputs into async iterables for batch parsing flows.
100
136
  * Supports synchronous iterables, async iterables, fetch responses, readable streams, and
@@ -3,7 +3,7 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
5
  import type {LoaderContext, LoaderOptions, FetchLike} from '@loaders.gl/loader-utils';
6
- import {isObject} from '@loaders.gl/loader-utils';
6
+ import {createAuthenticatedFetch, isObject} from '@loaders.gl/loader-utils';
7
7
  import {fetchFile} from '../fetch/fetch-file';
8
8
  import {getGlobalLoaderOptions} from './option-utils';
9
9
 
@@ -14,28 +14,30 @@ import {getGlobalLoaderOptions} from './option-utils';
14
14
  */
15
15
  export function getFetchFunction(
16
16
  options?: LoaderOptions,
17
- context?: Omit<LoaderContext, 'fetch'> & Partial<Pick<LoaderContext, 'fetch'>>
17
+ context?: Omit<LoaderContext, 'fetch' | 'coreApi'> &
18
+ Partial<Pick<LoaderContext, 'fetch' | 'coreApi'>>
18
19
  ): FetchLike {
19
20
  const globalOptions = getGlobalLoaderOptions();
20
21
 
21
22
  const loaderOptions = options || globalOptions;
22
23
  const fetchOption = loaderOptions.fetch ?? loaderOptions.core?.fetch;
24
+ let fetchFunction: FetchLike;
23
25
 
24
26
  // options.fetch can be a function
25
27
  if (typeof fetchOption === 'function') {
26
- return fetchOption;
28
+ fetchFunction = fetchOption;
29
+ } else if (isObject(fetchOption)) {
30
+ fetchFunction = context?.fetch || fetchFile;
31
+ } else if (context?.fetch) {
32
+ fetchFunction = context.fetch;
33
+ } else {
34
+ fetchFunction = fetchFile;
27
35
  }
28
36
 
29
- // options.fetch can be an options object
30
- if (isObject(fetchOption)) {
31
- return (url) => fetchFile(url, fetchOption as RequestInit);
32
- }
33
-
34
- // else refer to context (from parent loader) if available
35
- if (context?.fetch) {
36
- return context?.fetch;
37
- }
38
-
39
- // else return the default fetch function
40
- return fetchFile;
37
+ return createAuthenticatedFetch({
38
+ fetch: fetchFunction,
39
+ credentials: loaderOptions.core?.credentials || [],
40
+ authentications: loaderOptions.core?.authentications,
41
+ fetchOptions: isObject(fetchOption) ? (fetchOption as RequestInit) : undefined
42
+ });
41
43
  }
@@ -6,9 +6,11 @@ import type {Loader, LoaderContext, StrictLoaderOptions} from '@loaders.gl/loade
6
6
  import {getFetchFunction} from './get-fetch-function';
7
7
  import {extractQueryString, stripQueryString} from '../utils/url-utils';
8
8
  import {path} from '@loaders.gl/loader-utils';
9
+ import {coreApi} from '../api/core-api';
9
10
 
10
11
  /** Properties for creating an updated context */
11
- type LoaderContextProps = Omit<LoaderContext, 'fetch'> & Partial<Pick<LoaderContext, 'fetch'>>;
12
+ type LoaderContextProps = Omit<LoaderContext, 'fetch' | 'coreApi'> &
13
+ Partial<Pick<LoaderContext, 'fetch' | 'coreApi'>>;
12
14
 
13
15
  /**
14
16
  * "sub" loaders invoked by other loaders get a "context" injected on `this`
@@ -17,22 +19,28 @@ type LoaderContextProps = Omit<LoaderContext, 'fetch'> & Partial<Pick<LoaderCont
17
19
  *
18
20
  * @param context
19
21
  * @param options
20
- * @param previousContext
22
+ * @param parentContext
21
23
  */
22
24
  export function getLoaderContext(
23
25
  context: LoaderContextProps,
24
26
  options: StrictLoaderOptions,
25
27
  parentContext: LoaderContext | null
26
28
  ): LoaderContext {
27
- // For recursive calls, we already have a context
29
+ // For recursive calls, preserve the context while applying this loader's options
28
30
  // TODO - add any additional loaders to context?
29
31
  if (parentContext) {
30
- return parentContext;
32
+ return {
33
+ ...parentContext,
34
+ coreApi: parentContext.coreApi || context.coreApi || coreApi,
35
+ _parse: parentContext._parse || context._parse,
36
+ fetch: getFetchFunction(options, parentContext)
37
+ };
31
38
  }
32
39
 
33
40
  const newContext: LoaderContext = {
41
+ ...context,
34
42
  fetch: getFetchFunction(options, context),
35
- ...context
43
+ coreApi: context.coreApi || coreApi
36
44
  };
37
45
 
38
46
  // Parse URLs so that subloaders can easily generate correct strings
@@ -6,42 +6,68 @@
6
6
 
7
7
  import {Log} from '@probe.gl/log';
8
8
 
9
+ /** Default probe.gl logger used by loaders.gl. */
9
10
  export const probeLog = new Log({id: 'loaders.gl'});
10
11
 
11
12
  type LogFunction = () => void;
12
13
 
13
- // Logs nothing
14
+ /** Logger implementation that intentionally drops all messages. */
14
15
  export class NullLog {
16
+ /** Drops a log message. */
15
17
  log(): LogFunction {
16
18
  return () => {};
17
19
  }
20
+ /** Drops an info message. */
18
21
  info(): LogFunction {
19
22
  return () => {};
20
23
  }
24
+ /** Logs one message once; no-op for the null logger. */
25
+ once(): LogFunction {
26
+ return () => {};
27
+ }
28
+ /** Drops a warning message. */
21
29
  warn(): LogFunction {
22
30
  return () => {};
23
31
  }
32
+ /** Drops an error message. */
24
33
  error(): LogFunction {
25
34
  return () => {};
26
35
  }
27
36
  }
28
37
 
29
- // Logs to console
38
+ /** Logger implementation that forwards messages to the global console. */
30
39
  export class ConsoleLog {
40
+ /** Console-like object used for message output. */
31
41
  console;
42
+ /** Messages already emitted through `once`. */
43
+ private onceMessages = new Set<string>();
32
44
 
45
+ /** Creates a console-backed logger. */
33
46
  constructor() {
34
47
  this.console = console; // eslint-disable-line
35
48
  }
49
+ /** Logs a message to the console. */
36
50
  log(...args: unknown[]): LogFunction {
37
51
  return this.console.log.bind(this.console, ...args);
38
52
  }
53
+ /** Logs an informational message to the console. */
39
54
  info(...args: unknown[]): LogFunction {
40
55
  return this.console.info.bind(this.console, ...args);
41
56
  }
57
+ /** Logs one message once to the console. */
58
+ once(...args: unknown[]): LogFunction {
59
+ const message = String(args[0]);
60
+ if (this.onceMessages.has(message)) {
61
+ return () => {};
62
+ }
63
+ this.onceMessages.add(message);
64
+ return this.console.info.bind(this.console, ...args);
65
+ }
66
+ /** Logs a warning message to the console. */
42
67
  warn(...args: unknown[]): LogFunction {
43
68
  return this.console.warn.bind(this.console, ...args);
44
69
  }
70
+ /** Logs an error message to the console. */
45
71
  error(...args: unknown[]): LogFunction {
46
72
  return this.console.error.bind(this.console, ...args);
47
73
  }
@@ -7,6 +7,7 @@ import {isBrowser} from '@loaders.gl/loader-utils';
7
7
  import {ConsoleLog} from './loggers';
8
8
 
9
9
  export const DEFAULT_LOADER_OPTIONS: LoaderOptions = {
10
+ searchParams: undefined,
10
11
  core: {
11
12
  baseUrl: undefined,
12
13
  // baseUrl
@@ -15,11 +16,14 @@ export const DEFAULT_LOADER_OPTIONS: LoaderOptions = {
15
16
  fallbackMimeType: undefined,
16
17
  ignoreRegisteredLoaders: undefined,
17
18
  nothrow: false,
19
+ shape: undefined,
18
20
  log: new ConsoleLog(), // A probe.gl compatible (`log.log()()` syntax) that just logs to console
19
21
  useLocalLibraries: false,
22
+ subloaders: undefined,
20
23
 
21
24
  CDN: 'https://unpkg.com/@loaders.gl',
22
25
  worker: true, // By default, use worker if provided by loader.
26
+ workerThreshold: 0.1,
23
27
  maxConcurrency: 3, // How many worker instances should be created for each loader.
24
28
  maxMobileConcurrency: 1, // How many worker instances should be created for each loader on mobile devices.
25
29
  reuseWorkers: isBrowser, // By default reuse workers in browser (Node.js refuses to terminate if browsers are running)
@@ -57,6 +61,7 @@ export const REMOVED_LOADER_OPTIONS = {
57
61
 
58
62
  limit: 'core.limit',
59
63
  _limitMB: 'core._limitMB',
64
+ shape: 'core.shape',
60
65
  batchSize: 'core.batchSize',
61
66
  batchDebounceMs: 'core.batchDebounceMs',
62
67
  metadata: 'core.metadata',
@@ -26,19 +26,56 @@ const CORE_LOADER_OPTION_KEYS = [
26
26
  'useLocalLibraries',
27
27
  'CDN',
28
28
  'worker',
29
+ 'workerThreshold',
29
30
  'maxConcurrency',
30
31
  'maxMobileConcurrency',
31
32
  'reuseWorkers',
33
+ 'workerTransferBufferCopy',
32
34
  '_nodeWorkers',
33
35
  '_workerType',
34
36
  'limit',
35
37
  '_limitMB',
38
+ 'shape',
36
39
  'batchSize',
37
40
  'batchDebounceMs',
38
41
  'metadata',
39
42
  'transforms'
40
43
  ] as const;
41
44
 
45
+ /**
46
+ * Appends loader search parameters to a URL while preserving existing parameters and fragments.
47
+ * @param url URL to update
48
+ * @param options Loader options containing optional search parameters
49
+ * @returns URL with the configured parameters appended
50
+ */
51
+ export function applySearchParamsToUrl(url: string, options?: LoaderOptions): string {
52
+ const searchParams = options?.searchParams;
53
+ if (!searchParams || Object.keys(searchParams).length === 0 || /^(?:data|blob):/i.test(url)) {
54
+ return url;
55
+ }
56
+
57
+ const hashIndex = url.indexOf('#');
58
+ const hash = hashIndex >= 0 ? url.slice(hashIndex) : '';
59
+ const urlWithoutHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
60
+ const queryIndex = urlWithoutHash.indexOf('?');
61
+ const path = queryIndex >= 0 ? urlWithoutHash.slice(0, queryIndex) : urlWithoutHash;
62
+ const query = queryIndex >= 0 ? urlWithoutHash.slice(queryIndex + 1) : '';
63
+ const existingSearchParams = new URLSearchParams(query);
64
+ const appendedSearchParams = new URLSearchParams();
65
+ for (const [key, value] of Object.entries(searchParams)) {
66
+ if (!existingSearchParams.has(key)) {
67
+ appendedSearchParams.set(key, String(value));
68
+ }
69
+ }
70
+
71
+ const serializedAppendedSearchParams = appendedSearchParams.toString();
72
+ return `${path}${queryIndex >= 0 ? `?${query}` : ''}${
73
+ serializedAppendedSearchParams
74
+ ? `${queryIndex >= 0 ? '&' : '?'}${serializedAppendedSearchParams}`
75
+ : ''
76
+ }${hash}`;
77
+ }
78
+
42
79
  /**
43
80
  * Global state for loaders.gl. Stored on `globalThis.loaders._state`
44
81
  */
@@ -109,7 +146,10 @@ export function normalizeOptions(
109
146
  loaders = Array.isArray(loaders) ? loaders : [loaders];
110
147
 
111
148
  validateOptions(options, loaders);
112
- return normalizeLoaderOptions(normalizeOptionsInternal(loader, options, url));
149
+ validateWorkerThreshold(options);
150
+ const normalizedOptions = normalizeLoaderOptions(normalizeOptionsInternal(loader, options, url));
151
+ validateWorkerThreshold(normalizedOptions);
152
+ return normalizedOptions;
113
153
  }
114
154
 
115
155
  /**
@@ -157,6 +197,14 @@ function validateOptions(options: LoaderOptions, loaders: Loader[]): void {
157
197
  }
158
198
  }
159
199
 
200
+ /** Validates the automatic worker-selection threshold. */
201
+ function validateWorkerThreshold(options: LoaderOptions): void {
202
+ const threshold = options.core?.workerThreshold;
203
+ if (threshold !== undefined && (!Number.isFinite(threshold) || threshold < 0 || threshold > 1)) {
204
+ throw new Error('core.workerThreshold must be a finite number between 0 and 1');
205
+ }
206
+ }
207
+
160
208
  // eslint-disable-next-line max-params, complexity
161
209
  function validateOptionsObject(
162
210
  options: LoaderOptions,
@@ -235,6 +283,7 @@ function normalizeOptionsInternal(
235
283
 
236
284
  const userOptions = normalizeLoaderOptions(options);
237
285
  mergeNestedFields(mergedOptions, userOptions);
286
+ applyCoreShapeDefault(mergedOptions, userOptions, loader);
238
287
 
239
288
  addUrlOptions(mergedOptions, url);
240
289
  addDeprecatedTopLevelOptions(mergedOptions);
@@ -242,7 +291,50 @@ function normalizeOptionsInternal(
242
291
  return mergedOptions;
243
292
  }
244
293
 
245
- // Merge nested options objects
294
+ /**
295
+ * Applies `options.core.shape` to a loader's scoped `shape` option when that loader declares
296
+ * shape support and neither global nor user-supplied loader-scoped options override it.
297
+ * @param mergedOptions fully merged options object being normalized
298
+ * @param userOptions normalized user-supplied options
299
+ * @param loader selected loader
300
+ */
301
+ function applyCoreShapeDefault(
302
+ mergedOptions: LoaderOptions,
303
+ userOptions: LoaderOptions,
304
+ loader: Loader
305
+ ): void {
306
+ const coreShape = mergedOptions.core?.shape;
307
+ if (coreShape === undefined) {
308
+ return;
309
+ }
310
+
311
+ const loaderScopedDefaults = loader.options?.[loader.id];
312
+ if (!isPureObject(loaderScopedDefaults) || !('shape' in loaderScopedDefaults)) {
313
+ return;
314
+ }
315
+
316
+ const globalLoaderScopedOptions = getGlobalLoaderOptions()[loader.id];
317
+ if (isPureObject(globalLoaderScopedOptions) && 'shape' in globalLoaderScopedOptions) {
318
+ return;
319
+ }
320
+
321
+ const userLoaderScopedOptions = userOptions[loader.id];
322
+ if (isPureObject(userLoaderScopedOptions) && 'shape' in userLoaderScopedOptions) {
323
+ return;
324
+ }
325
+
326
+ const loaderScopedOptions = mergedOptions[loader.id];
327
+ mergedOptions[loader.id] = {
328
+ ...(isPureObject(loaderScopedOptions) ? loaderScopedOptions : {}),
329
+ shape: coreShape
330
+ };
331
+ }
332
+
333
+ /**
334
+ * Merges one level of nested option objects into the target options record.
335
+ * @param mergedOptions target options object to mutate
336
+ * @param options source options object
337
+ */
246
338
  function mergeNestedFields(mergedOptions: LoaderOptions, options: LoaderOptions): void {
247
339
  for (const key in options) {
248
340
  // Check for nested options
@@ -288,6 +380,11 @@ function cloneLoaderOptions(options: LoaderOptions): LoaderOptions {
288
380
  return clonedOptions;
289
381
  }
290
382
 
383
+ /**
384
+ * Moves deprecated top-level core option aliases into `options.core` without overwriting
385
+ * explicitly provided `options.core` values.
386
+ * @param options loader options to normalize in place
387
+ */
291
388
  function moveDeprecatedTopLevelOptionsToCore(options: LoaderOptions): void {
292
389
  if (options.baseUri !== undefined) {
293
390
  options.core ||= {};
@@ -298,7 +395,8 @@ function moveDeprecatedTopLevelOptionsToCore(options: LoaderOptions): void {
298
395
 
299
396
  for (const key of CORE_LOADER_OPTION_KEYS) {
300
397
  if ((options as Record<string, unknown>)[key] !== undefined) {
301
- const coreOptions = (options.core = options.core || {});
398
+ options.core ||= {};
399
+ const coreOptions = options.core;
302
400
  const coreRecord = coreOptions as Record<string, unknown>;
303
401
  // Treat deprecated top-level core options as aliases to `options.core`, but never override an explicitly
304
402
  // provided `options.core` value.
@@ -318,6 +416,11 @@ function moveDeprecatedTopLevelOptionsToCore(options: LoaderOptions): void {
318
416
  }
319
417
  }
320
418
 
419
+ /**
420
+ * Rehydrates deprecated top-level aliases from `options.core` for compatibility with older
421
+ * call sites that still read normalized options from the top level.
422
+ * @param options normalized loader options to mutate in place
423
+ */
321
424
  function addDeprecatedTopLevelOptions(options: LoaderOptions): void {
322
425
  const coreOptions = options.core as Record<string, unknown> | undefined;
323
426
  if (!coreOptions) {
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {isResponse} from '@loaders.gl/loader-utils';
5
+ import {encodeArrayBufferToBase64, isResponse} from '@loaders.gl/loader-utils';
6
6
  import {FetchError} from '../fetch/fetch-error';
7
7
  import {getResourceContentLength, getResourceUrl, getResourceMIMEType} from './resource-utils';
8
8
  import {shortenUrlForDisplay} from './url-utils';
@@ -97,7 +97,7 @@ async function getResponseError(response: Response): Promise<Error> {
97
97
  !response.bodyUsed && contentType?.includes('application/json')
98
98
  ? await response.json()
99
99
  : await response.text();
100
- } catch (error) {
100
+ } catch (_error) {
101
101
  // eslint forbids return in a finally statement, so we just catch here
102
102
  }
103
103
  return new FetchError(message, info);
@@ -112,26 +112,16 @@ async function getInitialDataUrl(
112
112
  }
113
113
  if (resource instanceof Blob) {
114
114
  const blobSlice = resource.slice(0, 5);
115
- return await new Promise((resolve) => {
115
+ return await new Promise(resolve => {
116
116
  const reader = new FileReader();
117
- reader.onload = (event) => resolve(event?.target?.result as string);
117
+ reader.onload = event => resolve(event?.target?.result as string);
118
118
  reader.readAsDataURL(blobSlice);
119
119
  });
120
120
  }
121
121
  if (resource instanceof ArrayBuffer) {
122
122
  const slice = resource.slice(0, INITIAL_DATA_LENGTH);
123
- const base64 = arrayBufferToBase64(slice);
123
+ const base64 = encodeArrayBufferToBase64(slice);
124
124
  return `data:base64,${base64}`;
125
125
  }
126
126
  return null;
127
127
  }
128
-
129
- // https://stackoverflow.com/questions/9267899/arraybuffer-to-base64-encoded-string
130
- function arrayBufferToBase64(buffer: ArrayBuffer): string {
131
- let binary = '';
132
- const bytes = new Uint8Array(buffer);
133
- for (let i = 0; i < bytes.byteLength; i++) {
134
- binary += String.fromCharCode(bytes[i]);
135
- }
136
- return btoa(binary);
137
- }
@@ -2,13 +2,13 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
+ import type {Loader, LoaderWithParser, LoaderOptions} from '@loaders.gl/loader-utils';
6
+ import {LoaderContext} from '@loaders.gl/loader-utils';
7
+
5
8
  // __VERSION__ is injected by babel-plugin-version-inline
6
9
  // @ts-ignore TS2304: Cannot find name '__VERSION__'.
7
10
  const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
8
11
 
9
- import type {Loader, LoaderWithParser, LoaderOptions} from '@loaders.gl/loader-utils';
10
- import {LoaderContext} from '@loaders.gl/loader-utils';
11
-
12
12
  export type NullLoaderOptions = LoaderOptions & {
13
13
  null?: {};
14
14
  };