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,338 @@
1
+ /**
2
+ * Browser satisfaction — provider access against real engine provider objects.
3
+ *
4
+ * Two tiers, and which one answered is visible to the guest in
5
+ * `descriptor.costClass`:
6
+ *
7
+ * Tier A the engine's own ProviderAccessPort, when the engine exposes one.
8
+ * Walks the loaded tile cache; costClass 0/1. Reaching into a
9
+ * provider's private fields is ENGINE work — those fields move on
10
+ * every upstream pin advance, and the engine keeps them tested in
11
+ * its own gates. The SDK calls the public port and nothing else.
12
+ *
13
+ * Tier B public engine API only. Terrain via the engine's exported
14
+ * most-detailed sampler, which re-requests and re-decodes:
15
+ * costClass 2. Under the default maxCost of 1 this tier REFUSES with
16
+ * SDM_PROVIDER_E_UNSUPPORTED; bytes come only from a caller that
17
+ * explicitly raised its ceiling.
18
+ *
19
+ * Nothing here imports the engine. The scene and the sampler arrive as
20
+ * parameters — the pluggable-provider law applied to the adapter itself.
21
+ */
22
+
23
+ import {
24
+ PROVIDER_NO_DATA_F64,
25
+ ProviderAccessError,
26
+ ProviderCost,
27
+ ProviderEncoding,
28
+ ProviderError,
29
+ ProviderFlags,
30
+ normalizeRequestPositions,
31
+ } from "./providerAccessAbi.js";
32
+
33
+ function engineTerrainProvider(scene) {
34
+ return scene?.globe?.terrainProvider ?? scene?.terrainProvider ?? null;
35
+ }
36
+
37
+ function providerLabel(provider, fallback) {
38
+ if (!provider) return fallback;
39
+ return (
40
+ provider.providerName ??
41
+ provider.credit?.html ??
42
+ provider.constructor?.name ??
43
+ fallback
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Tier B terrain adapter: the exported most-detailed sampler.
49
+ *
50
+ * The sampler mutates `height` on Cartographic-like objects and leaves it
51
+ * `undefined` where there is no data. `undefined` cannot survive into a typed
52
+ * array — it silently becomes 0, i.e. sea level, under a ridge. The ABI's
53
+ * no-data sentinel is applied HERE, at the boundary, so no consumer inherits
54
+ * that defect.
55
+ */
56
+ function createSampledTerrainAdapter(options) {
57
+ const scene = options.scene;
58
+ const sampleMostDetailed = options.sampleTerrainMostDetailed;
59
+ const sample = options.sampleTerrain;
60
+ const cartographicFromRadians = options.cartographicFromRadians;
61
+ const id = options.id ?? "terrain.engine";
62
+
63
+ if (typeof cartographicFromRadians !== "function") {
64
+ throw new TypeError(
65
+ "The engine terrain adapter requires cartographicFromRadians(lon, lat).",
66
+ );
67
+ }
68
+
69
+ async function sampleHeights(positions, level) {
70
+ const cartographics = positions.map(([lon, lat]) =>
71
+ cartographicFromRadians(lon, lat),
72
+ );
73
+ const provider = engineTerrainProvider(scene);
74
+ if (!provider) {
75
+ throw new ProviderAccessError(
76
+ ProviderError.NO_PROVIDER,
77
+ "The scene has no terrain provider.",
78
+ { providerId: id },
79
+ );
80
+ }
81
+ if (Number.isInteger(level) && typeof sample === "function") {
82
+ return sample(provider, level, cartographics);
83
+ }
84
+ if (typeof sampleMostDetailed !== "function") {
85
+ throw new ProviderAccessError(
86
+ ProviderError.UNSUPPORTED,
87
+ "No terrain sampler was supplied to the engine adapter.",
88
+ { providerId: id },
89
+ );
90
+ }
91
+ return sampleMostDetailed(provider, cartographics);
92
+ }
93
+
94
+ function pack(sampled) {
95
+ const heights = new Float64Array(sampled.length);
96
+ let min = Infinity;
97
+ let max = -Infinity;
98
+ let partial = false;
99
+ for (let index = 0; index < sampled.length; index += 1) {
100
+ const height = sampled[index]?.height;
101
+ if (typeof height !== "number" || !Number.isFinite(height)) {
102
+ heights[index] = PROVIDER_NO_DATA_F64;
103
+ partial = true;
104
+ continue;
105
+ }
106
+ heights[index] = height;
107
+ if (height < min) min = height;
108
+ if (height > max) max = height;
109
+ }
110
+ return { heights, min, max, partial };
111
+ }
112
+
113
+ return {
114
+ id,
115
+ kind: "terrain",
116
+ name: providerLabel(engineTerrainProvider(scene), "Engine terrain"),
117
+ ready: !!engineTerrainProvider(scene),
118
+ minLevel: 0,
119
+ maxLevel: options.maxLevel ?? 0,
120
+ tileWidth: 0,
121
+ tileHeight: 0,
122
+ encoding: ProviderEncoding.HEIGHT_F64,
123
+ // The whole point of the cost class: this tier re-decodes, and says so.
124
+ costClass: ProviderCost.REDECODE,
125
+ credit: options.credit ?? "",
126
+ attributes: { tier: "B", surface: "public-sampler" },
127
+
128
+ availability(params) {
129
+ const provider = engineTerrainProvider(scene);
130
+ const availability = provider?.availability;
131
+ if (!availability?.isTileAvailable) return true;
132
+ return !!availability.isTileAvailable(params.level, params.x, params.y);
133
+ },
134
+
135
+ prefetch() {
136
+ // Camera-driven engines have no region prefetch. Reported, not invented.
137
+ return { requested: 0, pending: 0, supported: false };
138
+ },
139
+
140
+ async awaitReady() {
141
+ const globe = scene?.globe;
142
+ if (!globe) return { ready: false, pending: 0 };
143
+ return { ready: globe.tilesLoaded !== false, pending: 0 };
144
+ },
145
+
146
+ async acquireProfile(request) {
147
+ const positions = normalizeRequestPositions(request) ?? interpolate(request);
148
+ const level = Number.isInteger(request.level) ? request.level : undefined;
149
+ const { heights, min, max, partial } = pack(
150
+ await sampleHeights(positions, level),
151
+ );
152
+ const first = positions[0] ?? [0, 0];
153
+ const last = positions[positions.length - 1] ?? [0, 0];
154
+ return {
155
+ planes: [heights],
156
+ descriptor: {
157
+ encoding: ProviderEncoding.HEIGHT_F64,
158
+ width: heights.length,
159
+ height: 1,
160
+ level,
161
+ minValue: Number.isFinite(min) ? min : 0,
162
+ maxValue: Number.isFinite(max) ? max : 0,
163
+ flags: partial ? ProviderFlags.PARTIAL : 0,
164
+ west: Math.min(first[0], last[0]),
165
+ east: Math.max(first[0], last[0]),
166
+ south: Math.min(first[1], last[1]),
167
+ north: Math.max(first[1], last[1]),
168
+ costClass: ProviderCost.REDECODE,
169
+ },
170
+ };
171
+ },
172
+
173
+ async acquireRegion(request) {
174
+ const [west, south, east, north] = request.rectangle ?? [0, 0, 0, 0];
175
+ const width = Math.max(1, request.width | 0);
176
+ const height = Math.max(1, request.height | 0);
177
+ const positions = [];
178
+ for (let row = 0; row < height; row += 1) {
179
+ const lat = north + ((south - north) * row) / height;
180
+ for (let column = 0; column < width; column += 1) {
181
+ positions.push([west + ((east - west) * column) / width, lat]);
182
+ }
183
+ }
184
+ const level = Number.isInteger(request.level) ? request.level : undefined;
185
+ const packed = pack(await sampleHeights(positions, level));
186
+ return {
187
+ planes: [packed.heights],
188
+ descriptor: {
189
+ encoding: ProviderEncoding.HEIGHT_F64,
190
+ width,
191
+ height,
192
+ level,
193
+ minValue: Number.isFinite(packed.min) ? packed.min : 0,
194
+ maxValue: Number.isFinite(packed.max) ? packed.max : 0,
195
+ flags: packed.partial ? ProviderFlags.PARTIAL : 0,
196
+ west,
197
+ south,
198
+ east,
199
+ north,
200
+ costClass: ProviderCost.REDECODE,
201
+ },
202
+ };
203
+ },
204
+ };
205
+ }
206
+
207
+ function interpolate(request) {
208
+ const [lon0, lat0] = request.start ?? [0, 0];
209
+ const [lon1, lat1] = request.end ?? [0, 0];
210
+ const samples = Math.max(2, request.samples | 0 || 2);
211
+ const positions = new Array(samples);
212
+ const last = samples - 1;
213
+ for (let index = 0; index < samples; index += 1) {
214
+ const t = index / last;
215
+ positions[index] = [lon0 + (lon1 - lon0) * t, lat0 + (lat1 - lat0) * t];
216
+ }
217
+ return positions;
218
+ }
219
+
220
+ /**
221
+ * Imagery through the public engine surface.
222
+ *
223
+ * CONTROL is fully native and fully supported: the imagery layer collection
224
+ * enumerates, orders and configures layers exactly as the engine already does.
225
+ *
226
+ * DATA is not. The engine releases an imagery tile's CPU pixel buffer as soon
227
+ * as the texture is uploaded, so for any tile the renderer has finished with,
228
+ * the decoded pixels are gone. This adapter therefore refuses resident pixel
229
+ * reads with SDM_PROVIDER_E_UNSUPPORTED rather than re-fetching behind the
230
+ * caller's back. Serving them cheaply requires tapping the pixels BEFORE
231
+ * upload, which is an engine change and belongs to the engine's owner.
232
+ */
233
+ function createImageryLayerAdapters(scene) {
234
+ const collection = scene?.imageryLayers;
235
+ if (!collection || typeof collection.get !== "function") return [];
236
+ const adapters = [];
237
+ const length = collection.length ?? 0;
238
+ for (let index = 0; index < length; index += 1) {
239
+ const layer = collection.get(index);
240
+ if (!layer) continue;
241
+ const provider = layer.imageryProvider;
242
+ const id = `imagery.layer.${index}`;
243
+ adapters.push({
244
+ id,
245
+ kind: "imagery",
246
+ name: providerLabel(provider, `Imagery layer ${index}`),
247
+ ready: true,
248
+ minLevel: provider?.minimumLevel ?? 0,
249
+ maxLevel: provider?.maximumLevel ?? 0,
250
+ tileWidth: provider?.tileWidth ?? 0,
251
+ tileHeight: provider?.tileHeight ?? 0,
252
+ encoding: ProviderEncoding.RGBA8,
253
+ costClass: ProviderCost.READBACK,
254
+ credit: provider?.credit?.html ?? "",
255
+ attributes: { tier: "B", surface: "imagery-layer", index },
256
+
257
+ availability(params) {
258
+ const availability = provider?.availability;
259
+ if (!availability?.isTileAvailable) return true;
260
+ return !!availability.isTileAvailable(params.level, params.x, params.y);
261
+ },
262
+
263
+ select() {
264
+ if (typeof collection.raiseToTop === "function") {
265
+ collection.raiseToTop(layer);
266
+ }
267
+ },
268
+
269
+ configure(settings = {}) {
270
+ const applied = [];
271
+ const rejected = [];
272
+ // Native layer properties only. An adapter must never emulate a
273
+ // setting the underlying surface does not have.
274
+ const native = [
275
+ "show",
276
+ "alpha",
277
+ "brightness",
278
+ "contrast",
279
+ "hue",
280
+ "saturation",
281
+ "gamma",
282
+ ];
283
+ for (const [key, value] of Object.entries(settings)) {
284
+ if (native.includes(key) && key in layer) {
285
+ layer[key] = value;
286
+ applied.push(key);
287
+ } else {
288
+ rejected.push(key);
289
+ }
290
+ }
291
+ return { applied, rejected };
292
+ },
293
+
294
+ prefetch() {
295
+ return { requested: 0, pending: 0, supported: false };
296
+ },
297
+
298
+ acquireTile() {
299
+ throw new ProviderAccessError(
300
+ ProviderError.UNSUPPORTED,
301
+ "Imagery pixels are released when the tile is uploaded to the GPU; " +
302
+ "resident pixel reads need an engine-side pre-upload tap. " +
303
+ "Raise maxCost to opt into a re-fetch or a GPU readback.",
304
+ { providerId: id },
305
+ );
306
+ },
307
+ });
308
+ }
309
+ return adapters;
310
+ }
311
+
312
+ /**
313
+ * Build provider adapters from a live engine scene.
314
+ *
315
+ * `scene.providerAccessPort` (Tier A), when the engine exposes it, is used
316
+ * verbatim: the engine owns private-field access, the SDK owns the wasm ABI.
317
+ */
318
+ export function createEngineProviderAdapters(options = {}) {
319
+ const scene = options.scene;
320
+ if (!scene) {
321
+ throw new TypeError("createEngineProviderAdapters requires a scene.");
322
+ }
323
+
324
+ const enginePort = scene.providerAccessPort ?? options.providerAccessPort;
325
+ if (enginePort && typeof enginePort.adapters === "function") {
326
+ return enginePort.adapters();
327
+ }
328
+ if (Array.isArray(enginePort?.adapters)) {
329
+ return enginePort.adapters;
330
+ }
331
+
332
+ const adapters = [];
333
+ if (typeof options.cartographicFromRadians === "function") {
334
+ adapters.push(createSampledTerrainAdapter({ ...options, scene }));
335
+ }
336
+ adapters.push(...createImageryLayerAdapters(scene));
337
+ return adapters;
338
+ }