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.
- 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 +39 -4
- 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/src/transport/pki.js +57 -7
- 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
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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>;
|