space-data-module-sdk 0.8.6 → 0.8.8

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 (51) hide show
  1. package/README.md +13 -0
  2. package/docs/AGENTS.md +5 -0
  3. package/docs/browser-wasmedge-isomorphic.md +65 -1
  4. package/docs/language-runtime-matrix.md +2 -2
  5. package/docs/provider-access-abi.md +600 -0
  6. package/package.json +15 -6
  7. package/schemas/orbpro/Propagator.fbs +19 -3
  8. package/src/browser.js +14 -4
  9. package/src/bundle/artifactBytes.js +44 -0
  10. package/src/bundle/index.js +1 -0
  11. package/src/compiler/compileModule.js +24 -1
  12. package/src/flow/flowCompiler.js +141 -2
  13. package/src/flow/flowRuntimeHost.js +7 -1
  14. package/src/flow/isomorphicFlowHost.js +1 -1
  15. package/src/flow/vendor/sdn-flow/MethodRegistry.js +6 -1
  16. package/src/generated/orbpro/propagator/propagator-source-description.js +2 -2
  17. package/src/generated/orbpro/propagator/propagator-source-description.ts +2 -2
  18. package/src/generated/orbpro/propagator/propagator-source-kind.js +13 -2
  19. package/src/generated/orbpro/propagator/propagator-source-kind.ts +13 -2
  20. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts +18 -1
  21. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts.map +1 -1
  22. package/src/generated/spacedatastandards/plg/pluginCategory.js +17 -0
  23. package/src/generated/spacedatastandards/plg/pluginCategory.ts +21 -1
  24. package/src/{testing → host}/browserModuleHarness.js +69 -34
  25. package/src/host/index.js +6 -0
  26. package/src/host/isomorphicLoader.js +17 -45
  27. package/src/host/isomorphicLoaderBrowser.js +47 -0
  28. package/src/host/isomorphicLoaderCore.js +58 -0
  29. package/src/host/nodeBuiltinSpecifier.js +43 -0
  30. package/src/host/providerAccess.js +727 -0
  31. package/src/host/providerAccessAbi.js +403 -0
  32. package/src/host/providerAccessEngineAdapter.js +338 -0
  33. package/src/host/providerAccessFixtureAdapter.js +444 -0
  34. package/src/host/providerAccessTileStoreAdapter.js +366 -0
  35. package/src/host/sabHostcallChannel.js +1 -1
  36. package/src/host/terrainSourceSeam.js +205 -0
  37. package/src/host/wasiThreadHost.js +11 -1
  38. package/src/{testing → host}/workerModuleHarness.js +45 -12
  39. package/src/{testing → host}/workerModuleHarnessWorker.js +6 -5
  40. package/src/index.d.ts +29 -2
  41. package/src/standards/browser.js +95 -0
  42. package/src/standards/catalogCore.js +290 -0
  43. package/src/standards/index.js +27 -251
  44. package/src/testing/AGENTS.md +25 -2
  45. package/src/testing/browser.js +32 -0
  46. package/src/testing/index.d.ts +12 -1
  47. package/src/testing/index.js +3 -3
  48. package/src/testing/parityBrowserRunner.js +9 -2
  49. package/src/testing/parityHarness.js +1 -1
  50. package/templates/provider-access-module/include/space_data_provider_abi.h +227 -0
  51. /package/src/{testing → host}/moduleFlatbufferStreamPump.js +0 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * WasmEdge satisfaction — a host-side tile store behind the same port.
3
+ *
4
+ * THE RULING. Under WasmEdge, native or in Docker, there is no engine. The
5
+ * server-side satisfaction of this port is a host-side TILE STORE, not a
6
+ * refusal and not an engine port. It serves the identical operations from
7
+ * sources the host can reach, and it does so using ONLY capabilities that
8
+ * already exist — `filesystem` for a local tileset directory, `http` for a
9
+ * remote tile service. It introduces no new generic hook, so it needs no owner
10
+ * sign-off and no new connector.
11
+ *
12
+ * The refusal case still exists and is still first-class: a host with no tile
13
+ * store configured registers no adapter, `provider.list` succeeds with an
14
+ * empty array, and `acquire` answers SDM_PROVIDER_E_NO_PROVIDER — the same
15
+ * code a browser gives for an unknown provider id, reachable and therefore
16
+ * testable in every lane.
17
+ *
18
+ * Node-side wiring of a specific curated source is CONFIGURATION of this
19
+ * adapter, never a change to the ABI.
20
+ */
21
+
22
+ import {
23
+ PROVIDER_NO_DATA_F64,
24
+ ProviderAccessError,
25
+ ProviderCost,
26
+ ProviderEncoding,
27
+ ProviderError,
28
+ ProviderFlags,
29
+ normalizeRequestPositions,
30
+ resolveRequestLevel,
31
+ } from "./providerAccessAbi.js";
32
+
33
+ const TWO_PI = 6.283185307179586;
34
+ const PI_OVER_TWO = 1.5707963267948966;
35
+
36
+ /**
37
+ * A tile store is anything that can answer "give me the decoded elements of
38
+ * tile (level,x,y)". The transport is the caller's business — a preopened
39
+ * directory read through the `filesystem` capability, an HTTP tile service
40
+ * through the `http` capability, or an in-memory map.
41
+ *
42
+ * readTile(level, x, y) -> { elements: TypedArray, width, height } | null
43
+ *
44
+ * Returning null means "not available" and becomes E_NOT_AVAILABLE. Throwing
45
+ * with `.code` set selects a specific ABI code; anything else becomes E_HOST
46
+ * with the detail on `provider.lastError`.
47
+ */
48
+ export function createTileStoreTerrainProvider(options = {}) {
49
+ const store = options.store;
50
+ if (!store || typeof store.readTile !== "function") {
51
+ throw new TypeError(
52
+ "createTileStoreTerrainProvider requires a store with readTile(level, x, y).",
53
+ );
54
+ }
55
+ const id = options.id ?? "terrain.tilestore";
56
+ const tileWidth = options.tileWidth ?? 65;
57
+ const tileHeight = options.tileHeight ?? 65;
58
+ const maxLevel = options.maxLevel ?? 14;
59
+ const defaultLevel = options.defaultLevel ?? 9;
60
+ // A store that decodes on read costs REDECODE; one that memoizes decoded
61
+ // tiles costs RESIDENT. The store declares it; the adapter never guesses.
62
+ const costClass = options.costClass ?? ProviderCost.RESIDENT;
63
+
64
+ // Shared with every other adapter, so a spacing request resolves to the SAME
65
+ // level in the browser and on the host. Two adapters doing their own level
66
+ // arithmetic would sample different ground and lose byte parity for a reason
67
+ // no diff would show.
68
+ function resolveLevel(request) {
69
+ return resolveRequestLevel(request, {
70
+ tileWidth,
71
+ maxLevel,
72
+ minLevel: options.minLevel ?? 0,
73
+ defaultLevel,
74
+ mostDetailedLevel: options.mostDetailedLevel ?? maxLevel,
75
+ });
76
+ }
77
+
78
+ function loadTile(level, x, y) {
79
+ const tile = store.readTile(level, x, y);
80
+ return thenish(tile, (resolved) => {
81
+ if (!resolved) {
82
+ throw new ProviderAccessError(
83
+ ProviderError.NOT_AVAILABLE,
84
+ `Tile ${level}/${x}/${y} is not present in the store.`,
85
+ { providerId: id },
86
+ );
87
+ }
88
+ return resolved;
89
+ });
90
+ }
91
+
92
+ function sampleTile(tile, i, j) {
93
+ const width = tile.width ?? tileWidth;
94
+ return tile.elements[j * width + i];
95
+ }
96
+
97
+ return {
98
+ id,
99
+ kind: "terrain",
100
+ name: options.name ?? "Host tile store terrain",
101
+ ready: true,
102
+ minLevel: options.minLevel ?? 0,
103
+ maxLevel,
104
+ tileWidth,
105
+ tileHeight,
106
+ encoding: ProviderEncoding.HEIGHT_F32,
107
+ costClass,
108
+ credit: options.credit ?? "",
109
+ attributes: { surface: "tile-store" },
110
+
111
+ availability(params) {
112
+ if (typeof store.hasTile === "function") {
113
+ return store.hasTile(params.level, params.x, params.y);
114
+ }
115
+ return params.level <= maxLevel;
116
+ },
117
+
118
+ prefetch(params) {
119
+ // A tile store has no camera. It can genuinely prefetch, and says so.
120
+ if (typeof store.prefetch === "function") return store.prefetch(params);
121
+ return { requested: 0, pending: 0 };
122
+ },
123
+
124
+ awaitReady() {
125
+ return { ready: true, pending: 0 };
126
+ },
127
+
128
+ acquireTile(request) {
129
+ const { level, strategyCode } = resolveLevel(request);
130
+ const x = request.x | 0;
131
+ const y = request.y | 0;
132
+ return thenish(loadTile(level, x, y), (tile) => {
133
+ const elements =
134
+ tile.elements instanceof Float32Array
135
+ ? tile.elements
136
+ : Float32Array.from(tile.elements);
137
+ let min = Infinity;
138
+ let max = -Infinity;
139
+ for (const value of elements) {
140
+ if (value < min) min = value;
141
+ if (value > max) max = value;
142
+ }
143
+ return {
144
+ planes: [elements],
145
+ descriptor: {
146
+ encoding: ProviderEncoding.HEIGHT_F32,
147
+ width: tile.width ?? tileWidth,
148
+ height: tile.height ?? tileHeight,
149
+ level,
150
+ tileX: x,
151
+ tileY: y,
152
+ minValue: Number.isFinite(min) ? min : 0,
153
+ maxValue: Number.isFinite(max) ? max : 0,
154
+ costClass,
155
+ ...tileRectangle(level, x, y),
156
+ },
157
+ };
158
+ });
159
+ },
160
+
161
+ async acquireProfile(request) {
162
+ const { level, strategyCode } = resolveLevel(request);
163
+ const positions = interpolate(request);
164
+ const heights = new Float64Array(positions.length);
165
+ const cache = new Map();
166
+ let min = Infinity;
167
+ let max = -Infinity;
168
+ let partial = false;
169
+
170
+ for (let index = 0; index < positions.length; index += 1) {
171
+ const [lon, lat] = positions[index];
172
+ const sample = toTileSample(lon, lat, level, tileWidth, tileHeight);
173
+ const key = `${sample.tileX}/${sample.tileY}`;
174
+ if (!cache.has(key)) {
175
+ try {
176
+ cache.set(key, await loadTile(level, sample.tileX, sample.tileY));
177
+ } catch (error) {
178
+ if (error?.code !== ProviderError.NOT_AVAILABLE) throw error;
179
+ cache.set(key, null);
180
+ }
181
+ }
182
+ const tile = cache.get(key);
183
+ if (!tile) {
184
+ heights[index] = PROVIDER_NO_DATA_F64;
185
+ partial = true;
186
+ continue;
187
+ }
188
+ const value = sampleTile(tile, sample.i, sample.j);
189
+ heights[index] = value;
190
+ if (value < min) min = value;
191
+ if (value > max) max = value;
192
+ }
193
+
194
+ const first = positions[0];
195
+ const last = positions[positions.length - 1];
196
+ return {
197
+ planes: [heights],
198
+ descriptor: {
199
+ encoding: ProviderEncoding.HEIGHT_F64,
200
+ width: positions.length,
201
+ height: 1,
202
+ level,
203
+ minValue: Number.isFinite(min) ? min : 0,
204
+ maxValue: Number.isFinite(max) ? max : 0,
205
+ flags: partial ? ProviderFlags.PARTIAL : 0,
206
+ west: Math.min(first[0], last[0]),
207
+ east: Math.max(first[0], last[0]),
208
+ south: Math.min(first[1], last[1]),
209
+ north: Math.max(first[1], last[1]),
210
+ costClass,
211
+ strategy: strategyCode,
212
+ },
213
+ };
214
+ },
215
+
216
+ async acquireRegion(request) {
217
+ const { level, strategyCode } = resolveLevel(request);
218
+ const [west, south, east, north] = request.rectangle ?? [0, 0, 0, 0];
219
+ const width = Math.max(1, request.width | 0);
220
+ const height = Math.max(1, request.height | 0);
221
+ const values = new Float32Array(width * height);
222
+ const cache = new Map();
223
+ let min = Infinity;
224
+ let max = -Infinity;
225
+ let partial = false;
226
+
227
+ for (let row = 0; row < height; row += 1) {
228
+ const lat = north + ((south - north) * row) / height;
229
+ for (let column = 0; column < width; column += 1) {
230
+ const lon = west + ((east - west) * column) / width;
231
+ const sample = toTileSample(lon, lat, level, tileWidth, tileHeight);
232
+ const key = `${sample.tileX}/${sample.tileY}`;
233
+ if (!cache.has(key)) {
234
+ try {
235
+ cache.set(key, await loadTile(level, sample.tileX, sample.tileY));
236
+ } catch (error) {
237
+ if (error?.code !== ProviderError.NOT_AVAILABLE) throw error;
238
+ cache.set(key, null);
239
+ }
240
+ }
241
+ const tile = cache.get(key);
242
+ if (!tile) {
243
+ values[row * width + column] = -3.4028234663852886e38;
244
+ partial = true;
245
+ continue;
246
+ }
247
+ const value = sampleTile(tile, sample.i, sample.j);
248
+ values[row * width + column] = value;
249
+ if (value < min) min = value;
250
+ if (value > max) max = value;
251
+ }
252
+ }
253
+
254
+ return {
255
+ planes: [values],
256
+ descriptor: {
257
+ encoding: ProviderEncoding.HEIGHT_F32,
258
+ width,
259
+ height,
260
+ level,
261
+ minValue: Number.isFinite(min) ? min : 0,
262
+ maxValue: Number.isFinite(max) ? max : 0,
263
+ flags: partial ? ProviderFlags.PARTIAL : 0,
264
+ west,
265
+ south,
266
+ east,
267
+ north,
268
+ costClass,
269
+ strategy: strategyCode,
270
+ },
271
+ };
272
+ },
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Tile store backed by the SDK host's `filesystem` capability.
278
+ *
279
+ * Uses only an existing generic hook. `decode(bytes, level, x, y)` is supplied
280
+ * by the caller because a tile format is not the ABI's business.
281
+ */
282
+ export function createFilesystemTileStore(options = {}) {
283
+ const host = options.host;
284
+ const root = String(options.root ?? "").replace(/\/+$/, "");
285
+ const decode = options.decode;
286
+ const template = options.template ?? "{level}/{x}/{y}.bin";
287
+ if (!host?.filesystem) {
288
+ throw new TypeError(
289
+ "createFilesystemTileStore requires a host with the filesystem capability.",
290
+ );
291
+ }
292
+ if (typeof decode !== "function") {
293
+ throw new TypeError(
294
+ "createFilesystemTileStore requires decode(bytes, level, x, y).",
295
+ );
296
+ }
297
+
298
+ function pathFor(level, x, y) {
299
+ return `${root}/${template
300
+ .replace("{level}", String(level))
301
+ .replace("{x}", String(x))
302
+ .replace("{y}", String(y))}`;
303
+ }
304
+
305
+ return {
306
+ async readTile(level, x, y) {
307
+ try {
308
+ const bytes = await host.filesystem.readFile(pathFor(level, x, y));
309
+ if (!bytes) return null;
310
+ return decode(bytes, level, x, y);
311
+ } catch (error) {
312
+ if (
313
+ error?.code === "ENOENT" ||
314
+ /not found|no such file/i.test(error?.message ?? "")
315
+ ) {
316
+ return null;
317
+ }
318
+ throw error;
319
+ }
320
+ },
321
+ };
322
+ }
323
+
324
+ function thenish(value, onValue) {
325
+ return value !== null && typeof value?.then === "function"
326
+ ? value.then(onValue)
327
+ : onValue(value);
328
+ }
329
+
330
+ function tileRectangle(level, x, y) {
331
+ const tiles = 1 << level;
332
+ const width = TWO_PI / (tiles * 2);
333
+ const height = Math.PI / tiles;
334
+ const west = -Math.PI + x * width;
335
+ const north = PI_OVER_TWO - y * height;
336
+ return { west, south: north - height, east: west + width, north };
337
+ }
338
+
339
+ function toTileSample(lon, lat, level, tileWidth, tileHeight) {
340
+ const tiles = 1 << level;
341
+ const gx = ((lon + Math.PI) / TWO_PI) * tiles * 2;
342
+ const gy = ((PI_OVER_TWO - lat) / Math.PI) * tiles;
343
+ const tileX = Math.min(Math.max(Math.floor(gx), 0), tiles * 2 - 1);
344
+ const tileY = Math.min(Math.max(Math.floor(gy), 0), tiles - 1);
345
+ return {
346
+ tileX,
347
+ tileY,
348
+ i: Math.min(Math.max(Math.floor((gx - tileX) * tileWidth), 0), tileWidth - 1),
349
+ j: Math.min(Math.max(Math.floor((gy - tileY) * tileHeight), 0), tileHeight - 1),
350
+ };
351
+ }
352
+
353
+ function interpolate(request) {
354
+ const explicit = normalizeRequestPositions(request);
355
+ if (explicit) return explicit;
356
+ const [lon0, lat0] = request.start ?? [0, 0];
357
+ const [lon1, lat1] = request.end ?? [0, 0];
358
+ const samples = Math.max(2, request.samples | 0 || 2);
359
+ const positions = new Array(samples);
360
+ const last = samples - 1;
361
+ for (let index = 0; index < samples; index += 1) {
362
+ const t = index / last;
363
+ positions[index] = [lon0 + (lon1 - lon0) * t, lat0 + (lat1 - lat0) * t];
364
+ }
365
+ return positions;
366
+ }
@@ -21,7 +21,7 @@
21
21
  * `createHostcallBridge({ dispatch })` (src/host/abi.js) — the existing
22
22
  * envelope/error machinery is reused unchanged. `Atomics.wait` is illegal on
23
23
  * the main thread, so the guest MUST run in a Worker (see
24
- * src/testing/workerModuleHarness.js).
24
+ * src/host/workerModuleHarness.js).
25
25
  *
26
26
  * Payloads cross the channel as hostcall value envelopes (hostcallWire.js),
27
27
  * so binary leaves (Uint8Array) survive without base64/JSON round-trips.
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Terrain source seam — the one interface a terrain consumer binds to.
3
+ *
4
+ * This exists so that the in-flight RF terrain solver does not ship a private
5
+ * terrain path. It is deliberately tiny and deliberately dual-shaped:
6
+ *
7
+ * readHeights(positions, out) the real entry — contiguous metres, one
8
+ * Float64Array, no per-sample JS object, no-data
9
+ * as the ABI sentinel rather than `undefined`
10
+ *
11
+ * sampleCompat(provider, ps) signature-compatible with the sampler the
12
+ * solver's injectable statics already hold, so
13
+ * the seam can be assigned on day one with no
14
+ * call-site change
15
+ *
16
+ * The cutover is therefore two recorded steps, not a rewrite: assign the seam
17
+ * now through the existing statics; move call sites to `readHeights` and make
18
+ * the source an explicit parameter when the engine port lands.
19
+ */
20
+
21
+ import {
22
+ PROVIDER_NO_DATA_F64,
23
+ ProviderCost,
24
+ ProviderFlags,
25
+ decodeTileDescriptor,
26
+ isProviderNoData,
27
+ providerStrategyName,
28
+ } from "./providerAccessAbi.js";
29
+
30
+ const PROVIDER_FLAG_PARTIAL = ProviderFlags.PARTIAL;
31
+ const PROVIDER_FLAG_INTERPOLATED = ProviderFlags.INTERPOLATED;
32
+
33
+ export const TERRAIN_SOURCE_NO_DATA = PROVIDER_NO_DATA_F64;
34
+
35
+ const REQUIRED_METHODS = Object.freeze([
36
+ "readHeights",
37
+ "readProfile",
38
+ "sampleCompat",
39
+ ]);
40
+ const REQUIRED_FIELDS = Object.freeze(["id", "costClass"]);
41
+
42
+ /**
43
+ * Prove at wiring time that whatever a consumer was handed is the real seam.
44
+ * A consumer that calls this cannot silently fall back to a private path,
45
+ * because a private path will not conform.
46
+ */
47
+ export function assertTerrainSourceConformance(source, label = "terrain source") {
48
+ const problems = [];
49
+ if (!source || typeof source !== "object") {
50
+ throw new TypeError(`${label} must be an object implementing the terrain source seam.`);
51
+ }
52
+ for (const field of REQUIRED_FIELDS) {
53
+ if (source[field] === undefined || source[field] === null) {
54
+ problems.push(`missing field "${field}"`);
55
+ }
56
+ }
57
+ for (const method of REQUIRED_METHODS) {
58
+ if (typeof source[method] !== "function") {
59
+ problems.push(`missing method "${method}()"`);
60
+ }
61
+ }
62
+ if (problems.length > 0) {
63
+ throw new TypeError(
64
+ `${label} does not conform to the terrain source seam: ${problems.join(", ")}. ` +
65
+ "See docs/provider-access-abi.md#consumer-seam--terrain-source.",
66
+ );
67
+ }
68
+ return source;
69
+ }
70
+
71
+ /**
72
+ * Wrap a provider access port as a terrain source.
73
+ *
74
+ * `maxCost` is passed through unchanged: a consumer that wants only resident
75
+ * bytes leaves it at the default and gets a refusal instead of a silent
76
+ * re-decode; a consumer that knowingly accepts the engine's re-decoding
77
+ * sampler raises it and says so in its own source.
78
+ */
79
+ export function createTerrainSourceFromPort(port, options = {}) {
80
+ if (!port || typeof port.invoke !== "function") {
81
+ throw new TypeError("createTerrainSourceFromPort requires a provider access port.");
82
+ }
83
+ const providerId = options.providerId ?? null;
84
+ const maxCost = Number.isInteger(options.maxCost)
85
+ ? options.maxCost
86
+ : ProviderCost.DEQUANTIZE;
87
+ const level = options.level ?? "mostDetailed";
88
+
89
+ async function acquireProfile(positions, callOptions = {}) {
90
+ const request = {
91
+ op: "profile",
92
+ positions: positions.map((position) => toRadianPair(position)),
93
+ maxCost: callOptions.maxCost ?? maxCost,
94
+ };
95
+ // Spacing wins over level when supplied: a consumer knows the stride it
96
+ // intends to march at, not a provider's level scheme.
97
+ const spacing = callOptions.spacing ?? options.spacing;
98
+ if (spacing !== undefined && spacing !== null) {
99
+ request.spacing = spacing;
100
+ } else {
101
+ request.level = callOptions.level ?? level;
102
+ }
103
+ if (providerId) request.providerId = providerId;
104
+ else request.kind = "terrain";
105
+
106
+ const { handle, descriptor } = await port.invoke("provider.acquire", request);
107
+ try {
108
+ const decoded = decodeTileDescriptor(descriptor);
109
+ const { bytes } = await port.invoke("provider.readRaw", {
110
+ handle,
111
+ plane: 0,
112
+ srcOffset: 0,
113
+ length: positions.length * 8,
114
+ });
115
+ return { bytes, decoded };
116
+ } finally {
117
+ await port.invoke("provider.release", { handle });
118
+ }
119
+ }
120
+
121
+ function strategyOf(callOptions = {}) {
122
+ const spacing = callOptions.spacing ?? options.spacing;
123
+ if (spacing !== undefined && spacing !== null) return "grid-matched-level";
124
+ return (callOptions.level ?? level) === "mostDetailed"
125
+ ? "most-detailed"
126
+ : "fixed-level";
127
+ }
128
+
129
+ function toHeights(bytes) {
130
+ return new Float64Array(
131
+ bytes.buffer,
132
+ bytes.byteOffset,
133
+ Math.floor(bytes.byteLength / 8),
134
+ );
135
+ }
136
+
137
+ return {
138
+ id: providerId ?? "terrain.port",
139
+ costClass: maxCost,
140
+
141
+ /** Bulk: contiguous metres. `out` is filled and returned when supplied. */
142
+ async readHeights(positions, out, callOptions = {}) {
143
+ const { bytes } = await acquireProfile(positions, callOptions);
144
+ const heights = toHeights(bytes);
145
+ if (!out) return heights.slice();
146
+ out.set(heights.subarray(0, out.length));
147
+ return out;
148
+ },
149
+
150
+ /**
151
+ * Heights PLUS provenance.
152
+ *
153
+ * A consumer that cannot see which level answered cannot tell a solve that
154
+ * resolved the ridges from one that interpolated them away, so the level
155
+ * actually used and how it was chosen come back with the data rather than
156
+ * being inferred from the request.
157
+ */
158
+ async readProfile(positions, callOptions = {}) {
159
+ const { bytes, decoded } = await acquireProfile(positions, callOptions);
160
+ const heights = toHeights(bytes);
161
+ return {
162
+ heights: callOptions.out
163
+ ? (callOptions.out.set(heights.subarray(0, callOptions.out.length)),
164
+ callOptions.out)
165
+ : heights.slice(),
166
+ level: decoded.level,
167
+ // Provenance comes from the DESCRIPTOR the adapter filled, so it
168
+ // reports what actually happened rather than what was asked for. The
169
+ // request-shaped guess is only a fallback for an adapter that has not
170
+ // been taught to report yet.
171
+ strategy: decoded.strategy
172
+ ? providerStrategyName(decoded.strategy)
173
+ : strategyOf(callOptions),
174
+ costClass: decoded.costClass,
175
+ partial: Boolean(decoded.flags & PROVIDER_FLAG_PARTIAL),
176
+ interpolated: Boolean(decoded.flags & PROVIDER_FLAG_INTERPOLATED),
177
+ };
178
+ },
179
+
180
+ /**
181
+ * Legacy shape. Mutates `height` on the supplied cartographics, exactly as
182
+ * the engine sampler does — including leaving no-data samples `undefined`,
183
+ * because that is what the existing call sites expect. New code should use
184
+ * readHeights and the sentinel.
185
+ */
186
+ async sampleCompat(_provider, positions) {
187
+ const heights = await this.readHeights(positions);
188
+ for (let index = 0; index < positions.length; index += 1) {
189
+ const height = heights[index];
190
+ positions[index].height = isProviderNoData(height) ? undefined : height;
191
+ }
192
+ return positions;
193
+ },
194
+ };
195
+ }
196
+
197
+ function toRadianPair(position) {
198
+ if (Array.isArray(position)) return [Number(position[0]), Number(position[1])];
199
+ if (typeof position?.longitude === "number") {
200
+ return [position.longitude, position.latitude];
201
+ }
202
+ throw new TypeError(
203
+ "A terrain sample position must be [lonRadians, latRadians] or a Cartographic.",
204
+ );
205
+ }
@@ -32,6 +32,8 @@
32
32
  //
33
33
  // See docs/isomorphic-pthreads.md and docs/browser-wasmedge-isomorphic.md.
34
34
 
35
+ import { NODE_BUILTIN_PREFIX } from "./nodeBuiltinSpecifier.js";
36
+
35
37
  const IS_NODE =
36
38
  typeof process !== "undefined" &&
37
39
  !!process.release &&
@@ -207,7 +209,15 @@ export async function createWasiThreadSpawn({
207
209
  // pthread_create time is fine here — there is no startup-vs-join deadlock.
208
210
  const workers = new Set();
209
211
  const osThreadIds = new Set();
210
- const workerThreads = await import("node:worker_threads");
212
+ // The specifier is assembled at runtime on purpose. This branch is dead in
213
+ // a browser, but a LITERAL `import("node:worker_threads")` is still
214
+ // statically resolved by esbuild/vite/rollup under a browser target, and
215
+ // the whole bundle fails to build. Keeping it opaque is what lets one host
216
+ // shim serve both runtimes; the browser branch below is the SAB+Worker one.
217
+ const nodeWorkerThreadsSpecifier = NODE_BUILTIN_PREFIX + "worker_threads";
218
+ const workerThreads = await import(
219
+ /* @vite-ignore */ /* webpackIgnore: true */ nodeWorkerThreadsSpecifier
220
+ );
211
221
  const NodeWorker = workerThreads.Worker;
212
222
  const nodeWorkerUrl = new URL("./wasiThreadWorker.mjs", import.meta.url);
213
223