space-data-module-sdk 0.8.5 → 0.8.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.
Files changed (34) hide show
  1. package/README.md +13 -0
  2. package/docs/AGENTS.md +5 -0
  3. package/docs/browser-wasmedge-isomorphic.md +64 -0
  4. package/docs/provider-access-abi.md +600 -0
  5. package/package.json +3 -2
  6. package/schemas/orbpro/Propagator.fbs +19 -3
  7. package/src/compiler/compileModule.js +24 -1
  8. package/src/flow/flowCompiler.js +141 -2
  9. package/src/flow/flowRuntimeHost.js +7 -1
  10. package/src/flow/vendor/sdn-flow/MethodRegistry.js +6 -1
  11. package/src/generated/orbpro/propagator/propagator-source-description.js +2 -2
  12. package/src/generated/orbpro/propagator/propagator-source-description.ts +2 -2
  13. package/src/generated/orbpro/propagator/propagator-source-kind.js +13 -2
  14. package/src/generated/orbpro/propagator/propagator-source-kind.ts +13 -2
  15. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts +18 -1
  16. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts.map +1 -1
  17. package/src/generated/spacedatastandards/plg/pluginCategory.js +17 -0
  18. package/src/generated/spacedatastandards/plg/pluginCategory.ts +21 -1
  19. package/src/host/index.js +6 -0
  20. package/src/host/providerAccess.js +727 -0
  21. package/src/host/providerAccessAbi.js +403 -0
  22. package/src/host/providerAccessEngineAdapter.js +338 -0
  23. package/src/host/providerAccessFixtureAdapter.js +444 -0
  24. package/src/host/providerAccessTileStoreAdapter.js +366 -0
  25. package/src/host/terrainSourceSeam.js +205 -0
  26. package/src/host/wasiThreadHost.js +12 -1
  27. package/src/index.d.ts +39 -4
  28. package/src/testing/browserModuleHarness.js +48 -0
  29. package/src/testing/index.d.ts +12 -1
  30. package/src/testing/parityBrowserRunner.js +8 -1
  31. package/src/testing/workerModuleHarness.js +36 -7
  32. package/src/testing/workerModuleHarnessWorker.js +5 -4
  33. package/src/transport/pki.js +57 -7
  34. package/templates/provider-access-module/include/space_data_provider_abi.h +227 -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
+ }
@@ -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
+ }
@@ -37,6 +37,9 @@ const IS_NODE =
37
37
  !!process.release &&
38
38
  process.release.name === "node";
39
39
 
40
+ // Split so no bundler can constant-fold it back into a literal `node:` import.
41
+ const NODE_BUILTIN_PREFIX = "no" + "de:";
42
+
40
43
  const BROWSER_WORKER_URL = new URL(
41
44
  "./wasiThreadBrowserWorker.mjs",
42
45
  import.meta.url,
@@ -207,7 +210,15 @@ export async function createWasiThreadSpawn({
207
210
  // pthread_create time is fine here — there is no startup-vs-join deadlock.
208
211
  const workers = new Set();
209
212
  const osThreadIds = new Set();
210
- const workerThreads = await import("node:worker_threads");
213
+ // The specifier is assembled at runtime on purpose. This branch is dead in
214
+ // a browser, but a LITERAL `import("node:worker_threads")` is still
215
+ // statically resolved by esbuild/vite/rollup under a browser target, and
216
+ // the whole bundle fails to build. Keeping it opaque is what lets one host
217
+ // shim serve both runtimes; the browser branch below is the SAB+Worker one.
218
+ const nodeWorkerThreadsSpecifier = NODE_BUILTIN_PREFIX + "worker_threads";
219
+ const workerThreads = await import(
220
+ /* @vite-ignore */ /* webpackIgnore: true */ nodeWorkerThreadsSpecifier
221
+ );
211
222
  const NodeWorker = workerThreads.Worker;
212
223
  const nodeWorkerUrl = new URL("./wasiThreadWorker.mjs", import.meta.url);
213
224
 
package/src/index.d.ts CHANGED
@@ -855,9 +855,16 @@ export function protectMarketplaceContent(options: {
855
855
  wrapNonce?: Uint8Array | ArrayBuffer | ArrayBufferView | number[] | null;
856
856
  }): Promise<MarketplaceProtectedContent>;
857
857
 
858
+ export type KeyAgreementProvider = (params: {
859
+ ephemeralPublicKey: Uint8Array;
860
+ context: string;
861
+ keyExchange: string;
862
+ }) => Promise<Uint8Array>;
863
+
858
864
  export function decryptMarketplaceContentKeyWrap(options: {
859
865
  wrap: MarketplaceContentKeyWrap;
860
- recipientPrivateKey: Uint8Array | ArrayBuffer | ArrayBufferView | number[] | string;
866
+ recipientPrivateKey?: Uint8Array | ArrayBuffer | ArrayBufferView | number[] | string;
867
+ keyAgreement?: KeyAgreementProvider;
861
868
  }): Promise<Uint8Array>;
862
869
 
863
870
  export function encryptBytesForRecipient(options: {
@@ -872,7 +879,8 @@ export function encryptBytesForRecipient(options: {
872
879
 
873
880
  export function decryptProtectedBytes(options: {
874
881
  protectedBytes: Uint8Array | ArrayBuffer;
875
- recipientPrivateKey: Uint8Array | string;
882
+ recipientPrivateKey?: Uint8Array | string;
883
+ keyAgreement?: KeyAgreementProvider;
876
884
  }): Promise<Uint8Array>;
877
885
 
878
886
  export function decryptBytesFromEnvelope(options: {
@@ -1954,7 +1962,11 @@ export interface BrowserModuleHarness {
1954
1962
  }
1955
1963
  export function detectArtifactProfile(wasmModule: WebAssembly.Module): string;
1956
1964
  export function createBrowserModuleHarness(options?: {
1957
- wasmSource: Uint8Array | ArrayBuffer | string | WebAssembly.Module | unknown;
1965
+ wasmSource: Uint8Array | ArrayBuffer | string | Response | WebAssembly.Module | unknown;
1966
+ // `true` uses env/global-configured trust; an object sets it explicitly;
1967
+ // `false` disables verification. Ignored for a precompiled WebAssembly.Module
1968
+ // (unverifiable — pass bytes/URL/Response when verification is required).
1969
+ verifySignature?: boolean | { trustedPublicKeys?: string[]; requireSignature?: boolean };
1958
1970
  host?: BrowserHost | RuntimeHost | Record<string, unknown>;
1959
1971
  hostOptions?: BrowserHostOptions;
1960
1972
  args?: string[];
@@ -1971,6 +1983,25 @@ export function createBrowserModuleHarness(options?: {
1971
1983
  allowRawInvoke?: boolean;
1972
1984
  initialMemoryBytes?: number;
1973
1985
  maximumMemoryBytes?: number;
1986
+ // Upper bound on guest wasi-threads spawns; sizes the browser warm worker
1987
+ // pool (defaults to hardware concurrency). No effect on a non-threaded guest.
1988
+ maxThreads?: number;
1989
+ // Request-isolated BroadcastChannel descriptor a nested pthread hostcall
1990
+ // dispatches over; supplied by createWorkerModuleHarness for the in-worker
1991
+ // harness instance, not something a top-level caller usually sets by hand.
1992
+ threadHostcallChannel?: {
1993
+ channelName: string;
1994
+ token: string;
1995
+ maxResponseBytes?: number;
1996
+ timeoutMs?: number;
1997
+ };
1998
+ // Explicit successful cross-origin-isolation negotiation for wasi-threads
1999
+ // guests; defaults to true. See docs/browser-wasmedge-isomorphic.md's
2000
+ // cross-origin-isolation section for when this is required.
2001
+ enableBrowserWasiThreads?: boolean;
2002
+ // Capacity of the module-memory arena a direct-invoke request is written
2003
+ // into (bytes). Defaults to 64 KiB.
2004
+ directInvokeRequestArenaBytes?: number;
1974
2005
  logOutput?: boolean;
1975
2006
  hostcallDispatch?: (operation: string, params: unknown) => unknown;
1976
2007
  }): Promise<BrowserModuleHarness>;
@@ -1989,7 +2020,11 @@ export interface WorkerModuleHarness {
1989
2020
  destroy(): Promise<void>;
1990
2021
  }
1991
2022
  export function createWorkerModuleHarness(options: {
1992
- wasmSource: Uint8Array | ArrayBuffer;
2023
+ // A precompiled WebAssembly.Module is preferred: it skips a redundant
2024
+ // compile and is structured-cloned to the worker directly (no plaintext
2025
+ // bytes cross the postMessage boundary at all). Uint8Array/ArrayBuffer is
2026
+ // compiled once on the controlling thread before the worker is spawned.
2027
+ wasmSource: WebAssembly.Module | Uint8Array | ArrayBuffer;
1993
2028
  host?: unknown;
1994
2029
  hostOptions?: BrowserHostOptions;
1995
2030
  dispatchHost?: (operation: string, params: unknown) => Promise<unknown>;