space-data-module-sdk 0.8.6 → 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.
- package/README.md +13 -0
- package/docs/AGENTS.md +5 -0
- package/docs/browser-wasmedge-isomorphic.md +64 -0
- package/docs/provider-access-abi.md +600 -0
- package/package.json +3 -2
- package/schemas/orbpro/Propagator.fbs +19 -3
- package/src/compiler/compileModule.js +24 -1
- package/src/flow/flowCompiler.js +141 -2
- package/src/flow/flowRuntimeHost.js +7 -1
- package/src/flow/vendor/sdn-flow/MethodRegistry.js +6 -1
- package/src/generated/orbpro/propagator/propagator-source-description.js +2 -2
- package/src/generated/orbpro/propagator/propagator-source-description.ts +2 -2
- package/src/generated/orbpro/propagator/propagator-source-kind.js +13 -2
- package/src/generated/orbpro/propagator/propagator-source-kind.ts +13 -2
- package/src/generated/spacedatastandards/plg/pluginCategory.d.ts +18 -1
- package/src/generated/spacedatastandards/plg/pluginCategory.d.ts.map +1 -1
- package/src/generated/spacedatastandards/plg/pluginCategory.js +17 -0
- package/src/generated/spacedatastandards/plg/pluginCategory.ts +21 -1
- package/src/host/index.js +6 -0
- package/src/host/providerAccess.js +727 -0
- package/src/host/providerAccessAbi.js +403 -0
- package/src/host/providerAccessEngineAdapter.js +338 -0
- package/src/host/providerAccessFixtureAdapter.js +444 -0
- package/src/host/providerAccessTileStoreAdapter.js +366 -0
- package/src/host/terrainSourceSeam.js +205 -0
- package/src/host/wasiThreadHost.js +12 -1
- package/src/index.d.ts +29 -2
- package/src/testing/browserModuleHarness.js +48 -0
- package/src/testing/index.d.ts +12 -1
- package/src/testing/parityBrowserRunner.js +8 -1
- package/src/testing/workerModuleHarness.js +36 -7
- package/src/testing/workerModuleHarnessWorker.js +5 -4
- 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
|
-
|
|
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
|
@@ -1962,7 +1962,11 @@ export interface BrowserModuleHarness {
|
|
|
1962
1962
|
}
|
|
1963
1963
|
export function detectArtifactProfile(wasmModule: WebAssembly.Module): string;
|
|
1964
1964
|
export function createBrowserModuleHarness(options?: {
|
|
1965
|
-
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 };
|
|
1966
1970
|
host?: BrowserHost | RuntimeHost | Record<string, unknown>;
|
|
1967
1971
|
hostOptions?: BrowserHostOptions;
|
|
1968
1972
|
args?: string[];
|
|
@@ -1979,6 +1983,25 @@ export function createBrowserModuleHarness(options?: {
|
|
|
1979
1983
|
allowRawInvoke?: boolean;
|
|
1980
1984
|
initialMemoryBytes?: number;
|
|
1981
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;
|
|
1982
2005
|
logOutput?: boolean;
|
|
1983
2006
|
hostcallDispatch?: (operation: string, params: unknown) => unknown;
|
|
1984
2007
|
}): Promise<BrowserModuleHarness>;
|
|
@@ -1997,7 +2020,11 @@ export interface WorkerModuleHarness {
|
|
|
1997
2020
|
destroy(): Promise<void>;
|
|
1998
2021
|
}
|
|
1999
2022
|
export function createWorkerModuleHarness(options: {
|
|
2000
|
-
|
|
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;
|
|
2001
2028
|
host?: unknown;
|
|
2002
2029
|
hostOptions?: BrowserHostOptions;
|
|
2003
2030
|
dispatchHost?: (operation: string, params: unknown) => Promise<unknown>;
|