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,403 @@
1
+ /**
2
+ * Provider Access ABI — constants, descriptor codec, and the guest import
3
+ * bridge. See docs/provider-access-abi.md for the normative contract.
4
+ *
5
+ * One generalized port for imagery/terrain providers, two planes:
6
+ *
7
+ * CONTROL provider.* operations on the existing space_data_module_host
8
+ * sync hostcall bridge (metadata only, JSON envelopes)
9
+ * DATA three dedicated imports on `space_data_provider` that write
10
+ * decoded provider bytes straight into guest linear memory
11
+ *
12
+ * Every parameter and every result across the boundary is i32. No i64 appears
13
+ * anywhere in a signature — the i64 legalization mismatch measured on this SDK
14
+ * is sidestepped by construction, not by convention. Every 64-bit quantity
15
+ * (rectangles, height extrema) travels inside the descriptor struct in guest
16
+ * memory as plain little-endian IEEE-754.
17
+ */
18
+
19
+ export const PROVIDER_IMPORT_MODULE = "space_data_provider";
20
+
21
+ /** Capability + scope. Both already exist; this ABI adds no new host capability. */
22
+ export const PROVIDER_CAPABILITY_ID = "scene_access";
23
+ export const PROVIDER_CAPABILITY_SCOPE = "provider.v1";
24
+
25
+ export const PROVIDER_ABI_VERSION = 1;
26
+ export const PROVIDER_TILE_DESC_MAGIC = 0x53445054; // 'SDPT'
27
+ export const PROVIDER_TILE_DESC_BYTES = 128;
28
+
29
+ export const ProviderKind = Object.freeze({
30
+ TERRAIN: 1,
31
+ IMAGERY: 2,
32
+ });
33
+
34
+ export const ProviderEncoding = Object.freeze({
35
+ HEIGHT_F32: 1,
36
+ HEIGHT_F64: 2,
37
+ RGBA8: 16,
38
+ RGB8: 17,
39
+ GRAY8: 18,
40
+ GRAY16: 19,
41
+ RGBA_F32: 20,
42
+ });
43
+
44
+ export const ProviderFlags = Object.freeze({
45
+ INTERPOLATED: 1 << 0,
46
+ STAGED: 1 << 1,
47
+ PARTIAL: 1 << 2,
48
+ DERIVED: 1 << 3,
49
+ FIXTURE: 1 << 4,
50
+ });
51
+
52
+ /**
53
+ * What an acquire actually cost. `maxCost` on the request defaults to
54
+ * DEQUANTIZE — that default IS the "never re-fetch, never re-parse" rule,
55
+ * enforced by the ABI instead of by discipline.
56
+ */
57
+ export const ProviderCost = Object.freeze({
58
+ RESIDENT: 0,
59
+ DEQUANTIZE: 1,
60
+ REDECODE: 2,
61
+ REFETCH: 3,
62
+ READBACK: 4,
63
+ });
64
+
65
+ export const DEFAULT_MAX_COST = ProviderCost.DEQUANTIZE;
66
+
67
+ /**
68
+ * How the level was chosen. Carried in the DESCRIPTOR, not just in a JS
69
+ * return value, because the consumers that most need provenance are wasm
70
+ * modules. Names mirror the consumer's existing vocabulary verbatim.
71
+ */
72
+ export const ProviderStrategy = Object.freeze({
73
+ DEFAULT: 0,
74
+ GRID_MATCHED_LEVEL: 1,
75
+ MOST_DETAILED: 2,
76
+ FIXED_LEVEL: 3,
77
+ });
78
+
79
+ const STRATEGY_NAMES = Object.freeze({
80
+ [ProviderStrategy.DEFAULT]: "default",
81
+ [ProviderStrategy.GRID_MATCHED_LEVEL]: "grid-matched-level",
82
+ [ProviderStrategy.MOST_DETAILED]: "most-detailed",
83
+ [ProviderStrategy.FIXED_LEVEL]: "fixed-level",
84
+ });
85
+
86
+ export function providerStrategyName(strategy) {
87
+ return STRATEGY_NAMES[strategy] ?? "default";
88
+ }
89
+
90
+ export const ProviderError = Object.freeze({
91
+ INVALID_REQUEST: -1,
92
+ NO_CAPABILITY: -2,
93
+ NO_PROVIDER: -3,
94
+ NOT_READY: -4,
95
+ NOT_AVAILABLE: -5,
96
+ BOUNDS: -6,
97
+ BAD_HANDLE: -7,
98
+ BAD_PLANE: -8,
99
+ UNSUPPORTED: -9,
100
+ TIMEOUT: -10,
101
+ HOST: -11,
102
+ PORT_UNAVAILABLE: -12,
103
+ });
104
+
105
+ const ERROR_NAMES = Object.freeze(
106
+ Object.fromEntries(
107
+ Object.entries(ProviderError).map(([name, code]) => [
108
+ code,
109
+ `SDM_PROVIDER_E_${name}`,
110
+ ]),
111
+ ),
112
+ );
113
+
114
+ export function providerErrorName(code) {
115
+ return ERROR_NAMES[code] ?? `SDM_PROVIDER_E_UNKNOWN(${code})`;
116
+ }
117
+
118
+ /**
119
+ * No-data sentinels. NaN is deliberately NOT used: WebAssembly does not
120
+ * canonicalize NaN payloads across every producing operation, so two runtimes
121
+ * can legitimately hold different bits for "a NaN" and a byte-identical-output
122
+ * assertion would fail on semantically equal values. -FLT_MAX / -DBL_MAX have
123
+ * exactly one encoding each and are never a real terrain height.
124
+ */
125
+ export const PROVIDER_NO_DATA_F32 = -3.4028234663852886e38; // 0xFF7FFFFF
126
+ export const PROVIDER_NO_DATA_F64 = -Number.MAX_VALUE; // 0xFFEFFFFFFFFFFFFF
127
+
128
+ export function isProviderNoData(value) {
129
+ return value === PROVIDER_NO_DATA_F32 || value === PROVIDER_NO_DATA_F64;
130
+ }
131
+
132
+ /** Bytes per element for an encoding, and elements per pixel/sample. */
133
+ const ENCODING_LAYOUT = Object.freeze({
134
+ [ProviderEncoding.HEIGHT_F32]: { bytes: 4, components: 1 },
135
+ [ProviderEncoding.HEIGHT_F64]: { bytes: 8, components: 1 },
136
+ [ProviderEncoding.RGBA8]: { bytes: 4, components: 4 },
137
+ [ProviderEncoding.RGB8]: { bytes: 3, components: 3 },
138
+ [ProviderEncoding.GRAY8]: { bytes: 1, components: 1 },
139
+ [ProviderEncoding.GRAY16]: { bytes: 2, components: 1 },
140
+ [ProviderEncoding.RGBA_F32]: { bytes: 16, components: 4 },
141
+ });
142
+
143
+ export function encodingLayout(encoding) {
144
+ const layout = ENCODING_LAYOUT[encoding];
145
+ if (!layout) {
146
+ throw new RangeError(`Unknown provider encoding ${encoding}.`);
147
+ }
148
+ return layout;
149
+ }
150
+
151
+ /**
152
+ * Level selection from a TARGET SAMPLE SPACING, in metres.
153
+ *
154
+ * Callers know the stride they intend to march at; they do not know a
155
+ * provider's level scheme. Asking for "most detailed" everywhere is what makes
156
+ * a solve slow, and asking for coarser than the march stride is what makes it
157
+ * wrong — a coverage solve sampling at its output raster's spacing instead of
158
+ * its profile's mis-reported a 400 m ridge by 27 dB. So the caller states
159
+ * spacing and the port picks the coarsest level that satisfies it.
160
+ *
161
+ * Defined HERE, once, and shared by every adapter: if the browser and the host
162
+ * tile store chose levels by their own arithmetic they would sample different
163
+ * ground and byte parity would be lost for a reason no one could see.
164
+ */
165
+ export const PROVIDER_EARTH_CIRCUMFERENCE_METERS = 40075016.6855785;
166
+
167
+ export function providerLevelForSpacing(spacingMeters, options = {}) {
168
+ const tileWidth = Math.max(2, options.tileWidth ?? 65);
169
+ const maxLevel = options.maxLevel ?? 20;
170
+ const minLevel = options.minLevel ?? 0;
171
+ const spacing = Number(spacingMeters);
172
+ if (!Number.isFinite(spacing) || spacing <= 0) return maxLevel;
173
+
174
+ for (let level = minLevel; level <= maxLevel; level += 1) {
175
+ // Level L has 2^(L+1) tiles around the equator, each covering
176
+ // (tileWidth - 1) sample intervals.
177
+ const tilesAcross = Math.pow(2, level + 1);
178
+ const levelSpacing =
179
+ PROVIDER_EARTH_CIRCUMFERENCE_METERS / (tilesAcross * (tileWidth - 1));
180
+ if (levelSpacing <= spacing) return level;
181
+ }
182
+ return maxLevel;
183
+ }
184
+
185
+ /**
186
+ * Resolve a request's level, and report HOW it was resolved. A consumer that
187
+ * cannot see which level answered cannot tell a solve that resolved the ridges
188
+ * from one that interpolated them away, so provenance is returned, never
189
+ * inferred.
190
+ */
191
+ export function resolveRequestLevel(request, options = {}) {
192
+ const fallback = options.defaultLevel ?? options.maxLevel ?? 0;
193
+ const spacing = request?.spacing ?? request?.targetSpacingMeters;
194
+ if (spacing !== undefined && spacing !== null) {
195
+ return {
196
+ level: providerLevelForSpacing(spacing, options),
197
+ // Strategy names match the consumer's existing vocabulary verbatim
198
+ // (RfTerrainAnalysis surfaces them in coverage metadata and in the demo
199
+ // legend). A port that renamed them would force every consumer to keep a
200
+ // translation table.
201
+ strategy: "grid-matched-level",
202
+ strategyCode: ProviderStrategy.GRID_MATCHED_LEVEL,
203
+ };
204
+ }
205
+ const level = request?.level;
206
+ if (level === "mostDetailed") {
207
+ return {
208
+ level: options.mostDetailedLevel ?? options.maxLevel ?? fallback,
209
+ strategy: "most-detailed",
210
+ strategyCode: ProviderStrategy.MOST_DETAILED,
211
+ };
212
+ }
213
+ if (level === undefined || level === null) {
214
+ return { level: fallback, strategy: "default", strategyCode: ProviderStrategy.DEFAULT };
215
+ }
216
+ const numeric = Number(level);
217
+ if (!Number.isInteger(numeric) || numeric < 0) {
218
+ return { level: fallback, strategy: "default", strategyCode: ProviderStrategy.DEFAULT };
219
+ }
220
+ return {
221
+ level: numeric,
222
+ strategy: "fixed-level",
223
+ strategyCode: ProviderStrategy.FIXED_LEVEL,
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Normalize a request's positions into [lon, lat] pairs.
229
+ *
230
+ * Accepts the JSON `positions` array OR the binary `positionsBuffer`
231
+ * (interleaved f64 lon/lat) that the guest bridge materializes from a pointer.
232
+ * Shared by every adapter so a raster field is read identically everywhere.
233
+ */
234
+ export function normalizeRequestPositions(request) {
235
+ const buffer = request?.positionsBuffer;
236
+ if (buffer && ArrayBuffer.isView(buffer)) {
237
+ const count = Math.floor(buffer.length / 2);
238
+ const positions = new Array(count);
239
+ for (let index = 0; index < count; index += 1) {
240
+ positions[index] = [buffer[index * 2], buffer[index * 2 + 1]];
241
+ }
242
+ return positions;
243
+ }
244
+ if (Array.isArray(request?.positions)) {
245
+ return request.positions.map((position) =>
246
+ Array.isArray(position)
247
+ ? [Number(position[0]), Number(position[1])]
248
+ : [Number(position.longitude), Number(position.latitude)],
249
+ );
250
+ }
251
+ return null;
252
+ }
253
+
254
+ /** FNV-1a 32. Stable provider-id hash, identical in every runtime. */
255
+ export function providerSourceId(id) {
256
+ let hash = 0x811c9dc5;
257
+ const text = String(id ?? "");
258
+ for (let index = 0; index < text.length; index += 1) {
259
+ hash ^= text.charCodeAt(index) & 0xff;
260
+ hash = Math.imul(hash, 0x01000193) >>> 0;
261
+ }
262
+ return hash >>> 0;
263
+ }
264
+
265
+ const DESC_OFFSETS = Object.freeze({
266
+ magic: 0,
267
+ version: 4,
268
+ kind: 8,
269
+ encoding: 12,
270
+ width: 16,
271
+ height: 20,
272
+ planeCount: 24,
273
+ bytesPerElement: 28,
274
+ rowStrideBytes: 32,
275
+ byteLength: 36,
276
+ flags: 40,
277
+ level: 44,
278
+ west: 48,
279
+ south: 56,
280
+ east: 64,
281
+ north: 72,
282
+ minValue: 80,
283
+ maxValue: 88,
284
+ tileX: 96,
285
+ tileY: 100,
286
+ hostCopies: 104,
287
+ sourceId: 108,
288
+ costClass: 112,
289
+ strategy: 116,
290
+ });
291
+
292
+ export const PROVIDER_LEVEL_NONE = 0xffffffff;
293
+
294
+ /**
295
+ * Serialize a tile descriptor into 128 little-endian bytes. Byte-identical for
296
+ * identical input in every runtime — this buffer is inside the parity envelope.
297
+ */
298
+ export function encodeTileDescriptor(desc) {
299
+ const bytes = new Uint8Array(PROVIDER_TILE_DESC_BYTES);
300
+ const view = new DataView(bytes.buffer);
301
+ const u32 = (offset, value) => view.setUint32(offset, value >>> 0, true);
302
+ const f64 = (offset, value) => view.setFloat64(offset, Number(value), true);
303
+
304
+ u32(DESC_OFFSETS.magic, PROVIDER_TILE_DESC_MAGIC);
305
+ u32(DESC_OFFSETS.version, PROVIDER_ABI_VERSION);
306
+ u32(DESC_OFFSETS.kind, desc.kind);
307
+ u32(DESC_OFFSETS.encoding, desc.encoding);
308
+ u32(DESC_OFFSETS.width, desc.width);
309
+ u32(DESC_OFFSETS.height, desc.height);
310
+ u32(DESC_OFFSETS.planeCount, desc.planeCount ?? 1);
311
+ u32(DESC_OFFSETS.bytesPerElement, desc.bytesPerElement);
312
+ u32(DESC_OFFSETS.rowStrideBytes, desc.rowStrideBytes);
313
+ u32(DESC_OFFSETS.byteLength, desc.byteLength);
314
+ u32(DESC_OFFSETS.flags, desc.flags ?? 0);
315
+ u32(DESC_OFFSETS.level, desc.level ?? PROVIDER_LEVEL_NONE);
316
+ f64(DESC_OFFSETS.west, desc.west ?? 0);
317
+ f64(DESC_OFFSETS.south, desc.south ?? 0);
318
+ f64(DESC_OFFSETS.east, desc.east ?? 0);
319
+ f64(DESC_OFFSETS.north, desc.north ?? 0);
320
+ f64(DESC_OFFSETS.minValue, desc.minValue ?? 0);
321
+ f64(DESC_OFFSETS.maxValue, desc.maxValue ?? 0);
322
+ u32(DESC_OFFSETS.tileX, desc.tileX ?? PROVIDER_LEVEL_NONE);
323
+ u32(DESC_OFFSETS.tileY, desc.tileY ?? PROVIDER_LEVEL_NONE);
324
+ u32(DESC_OFFSETS.hostCopies, desc.hostCopies ?? 1);
325
+ u32(DESC_OFFSETS.sourceId, desc.sourceId ?? 0);
326
+ u32(DESC_OFFSETS.costClass, desc.costClass ?? ProviderCost.RESIDENT);
327
+ u32(DESC_OFFSETS.strategy, desc.strategy ?? ProviderStrategy.DEFAULT);
328
+ return bytes;
329
+ }
330
+
331
+ /** Inverse of encodeTileDescriptor — used by tests and JS-side consumers. */
332
+ export function decodeTileDescriptor(bytes) {
333
+ if (bytes.byteLength < PROVIDER_TILE_DESC_BYTES) {
334
+ throw new RangeError(
335
+ `Tile descriptor requires ${PROVIDER_TILE_DESC_BYTES} bytes, got ${bytes.byteLength}.`,
336
+ );
337
+ }
338
+ const view = new DataView(
339
+ bytes.buffer,
340
+ bytes.byteOffset,
341
+ PROVIDER_TILE_DESC_BYTES,
342
+ );
343
+ const u32 = (offset) => view.getUint32(offset, true);
344
+ const f64 = (offset) => view.getFloat64(offset, true);
345
+ const magic = u32(DESC_OFFSETS.magic);
346
+ if (magic !== PROVIDER_TILE_DESC_MAGIC) {
347
+ throw new Error(
348
+ `Tile descriptor magic mismatch: expected 0x${PROVIDER_TILE_DESC_MAGIC.toString(16)}, got 0x${magic.toString(16)}.`,
349
+ );
350
+ }
351
+ return {
352
+ magic,
353
+ version: u32(DESC_OFFSETS.version),
354
+ kind: u32(DESC_OFFSETS.kind),
355
+ encoding: u32(DESC_OFFSETS.encoding),
356
+ width: u32(DESC_OFFSETS.width),
357
+ height: u32(DESC_OFFSETS.height),
358
+ planeCount: u32(DESC_OFFSETS.planeCount),
359
+ bytesPerElement: u32(DESC_OFFSETS.bytesPerElement),
360
+ rowStrideBytes: u32(DESC_OFFSETS.rowStrideBytes),
361
+ byteLength: u32(DESC_OFFSETS.byteLength),
362
+ flags: u32(DESC_OFFSETS.flags),
363
+ level: u32(DESC_OFFSETS.level),
364
+ west: f64(DESC_OFFSETS.west),
365
+ south: f64(DESC_OFFSETS.south),
366
+ east: f64(DESC_OFFSETS.east),
367
+ north: f64(DESC_OFFSETS.north),
368
+ minValue: f64(DESC_OFFSETS.minValue),
369
+ maxValue: f64(DESC_OFFSETS.maxValue),
370
+ tileX: u32(DESC_OFFSETS.tileX),
371
+ tileY: u32(DESC_OFFSETS.tileY),
372
+ hostCopies: u32(DESC_OFFSETS.hostCopies),
373
+ sourceId: u32(DESC_OFFSETS.sourceId),
374
+ costClass: u32(DESC_OFFSETS.costClass),
375
+ strategy: u32(DESC_OFFSETS.strategy),
376
+ };
377
+ }
378
+
379
+ /**
380
+ * Error thrown by adapters that want to select a specific ABI code. Anything
381
+ * else an adapter throws becomes SDM_PROVIDER_E_HOST with the detail available
382
+ * through `provider.lastError`.
383
+ */
384
+ export class ProviderAccessError extends Error {
385
+ constructor(code, message, options = {}) {
386
+ super(message);
387
+ this.name = "ProviderAccessError";
388
+ this.code = code;
389
+ this.providerErrorName = providerErrorName(code);
390
+ this.operation = options.operation ?? null;
391
+ this.providerId = options.providerId ?? null;
392
+ }
393
+ }
394
+
395
+ export function providerErrorCodeOf(error) {
396
+ if (error instanceof ProviderAccessError) {
397
+ return error.code;
398
+ }
399
+ if (typeof error?.code === "number" && error.code < 0 && error.code >= -12) {
400
+ return error.code;
401
+ }
402
+ return ProviderError.HOST;
403
+ }