space-data-module-sdk 0.8.6 → 0.8.8

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 (51) hide show
  1. package/README.md +13 -0
  2. package/docs/AGENTS.md +5 -0
  3. package/docs/browser-wasmedge-isomorphic.md +65 -1
  4. package/docs/language-runtime-matrix.md +2 -2
  5. package/docs/provider-access-abi.md +600 -0
  6. package/package.json +15 -6
  7. package/schemas/orbpro/Propagator.fbs +19 -3
  8. package/src/browser.js +14 -4
  9. package/src/bundle/artifactBytes.js +44 -0
  10. package/src/bundle/index.js +1 -0
  11. package/src/compiler/compileModule.js +24 -1
  12. package/src/flow/flowCompiler.js +141 -2
  13. package/src/flow/flowRuntimeHost.js +7 -1
  14. package/src/flow/isomorphicFlowHost.js +1 -1
  15. package/src/flow/vendor/sdn-flow/MethodRegistry.js +6 -1
  16. package/src/generated/orbpro/propagator/propagator-source-description.js +2 -2
  17. package/src/generated/orbpro/propagator/propagator-source-description.ts +2 -2
  18. package/src/generated/orbpro/propagator/propagator-source-kind.js +13 -2
  19. package/src/generated/orbpro/propagator/propagator-source-kind.ts +13 -2
  20. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts +18 -1
  21. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts.map +1 -1
  22. package/src/generated/spacedatastandards/plg/pluginCategory.js +17 -0
  23. package/src/generated/spacedatastandards/plg/pluginCategory.ts +21 -1
  24. package/src/{testing → host}/browserModuleHarness.js +69 -34
  25. package/src/host/index.js +6 -0
  26. package/src/host/isomorphicLoader.js +17 -45
  27. package/src/host/isomorphicLoaderBrowser.js +47 -0
  28. package/src/host/isomorphicLoaderCore.js +58 -0
  29. package/src/host/nodeBuiltinSpecifier.js +43 -0
  30. package/src/host/providerAccess.js +727 -0
  31. package/src/host/providerAccessAbi.js +403 -0
  32. package/src/host/providerAccessEngineAdapter.js +338 -0
  33. package/src/host/providerAccessFixtureAdapter.js +444 -0
  34. package/src/host/providerAccessTileStoreAdapter.js +366 -0
  35. package/src/host/sabHostcallChannel.js +1 -1
  36. package/src/host/terrainSourceSeam.js +205 -0
  37. package/src/host/wasiThreadHost.js +11 -1
  38. package/src/{testing → host}/workerModuleHarness.js +45 -12
  39. package/src/{testing → host}/workerModuleHarnessWorker.js +6 -5
  40. package/src/index.d.ts +29 -2
  41. package/src/standards/browser.js +95 -0
  42. package/src/standards/catalogCore.js +290 -0
  43. package/src/standards/index.js +27 -251
  44. package/src/testing/AGENTS.md +25 -2
  45. package/src/testing/browser.js +32 -0
  46. package/src/testing/index.d.ts +12 -1
  47. package/src/testing/index.js +3 -3
  48. package/src/testing/parityBrowserRunner.js +9 -2
  49. package/src/testing/parityHarness.js +1 -1
  50. package/templates/provider-access-module/include/space_data_provider_abi.h +227 -0
  51. /package/src/{testing → host}/moduleFlatbufferStreamPump.js +0 -0
@@ -0,0 +1,727 @@
1
+ /**
2
+ * Provider Access port — host side.
3
+ *
4
+ * `createProviderAccessPort({ adapters })` owns the provider registry, the
5
+ * pinned-buffer handle table, and every `provider.*` control operation. It is
6
+ * transport-agnostic: it never learns which runtime it is in, and it never
7
+ * touches guest memory.
8
+ *
9
+ * `createProviderAccessBridge({ getMemory, dispatch, directRead })` is the
10
+ * guest-facing half: the three `space_data_provider` imports. It is the ONLY
11
+ * place that knows the memory topology, and therefore the only place that
12
+ * decides whether a read costs one copy or two.
13
+ *
14
+ * See docs/provider-access-abi.md.
15
+ */
16
+
17
+ import {
18
+ DEFAULT_MAX_COST,
19
+ PROVIDER_IMPORT_MODULE,
20
+ PROVIDER_TILE_DESC_BYTES,
21
+ ProviderAccessError,
22
+ ProviderCost,
23
+ ProviderError,
24
+ ProviderFlags,
25
+ ProviderKind,
26
+ encodeTileDescriptor,
27
+ encodingLayout,
28
+ providerErrorCodeOf,
29
+ providerErrorName,
30
+ providerSourceId,
31
+ } from "./providerAccessAbi.js";
32
+
33
+ const textDecoder = new TextDecoder();
34
+
35
+ function isThenable(value) {
36
+ return value !== null && typeof value?.then === "function";
37
+ }
38
+
39
+ /**
40
+ * Continue on a value that MAY be a promise, without forcing one.
41
+ *
42
+ * This matters: a native host call under WasmEdge is synchronous, and if the
43
+ * port turned every synchronous adapter into a promise the guest bridge could
44
+ * never complete inside one import call. Adapters that are synchronous stay
45
+ * synchronous all the way to the guest; adapters that are asynchronous block
46
+ * on the transport (the SAB Atomics.wait channel in a browser worker), exactly
47
+ * as `http` and `storage` already do.
48
+ */
49
+ function then(value, onValue) {
50
+ return isThenable(value) ? value.then(onValue) : onValue(value);
51
+ }
52
+
53
+ const KIND_BY_NAME = Object.freeze({
54
+ terrain: ProviderKind.TERRAIN,
55
+ imagery: ProviderKind.IMAGERY,
56
+ });
57
+
58
+ function normalizeKind(kind) {
59
+ if (kind === null || kind === undefined || kind === "") return null;
60
+ if (typeof kind === "number") return kind;
61
+ const normalized = KIND_BY_NAME[String(kind).toLowerCase()];
62
+ if (!normalized) {
63
+ throw new ProviderAccessError(
64
+ ProviderError.INVALID_REQUEST,
65
+ `Unknown provider kind "${kind}".`,
66
+ );
67
+ }
68
+ return normalized;
69
+ }
70
+
71
+ function assertWithinCost(adapter, costClass, request) {
72
+ const maxCost = Number.isInteger(request?.maxCost)
73
+ ? request.maxCost
74
+ : DEFAULT_MAX_COST;
75
+ if (costClass > maxCost) {
76
+ throw new ProviderAccessError(
77
+ ProviderError.UNSUPPORTED,
78
+ `Provider "${adapter.id}" can only serve this at cost class ${costClass} ` +
79
+ `(${maxCost} allowed). Raise "maxCost" to opt in explicitly.`,
80
+ { providerId: adapter.id },
81
+ );
82
+ }
83
+ }
84
+
85
+ function requireAdapterMethod(adapter, method, operation) {
86
+ if (typeof adapter[method] !== "function") {
87
+ throw new ProviderAccessError(
88
+ ProviderError.UNSUPPORTED,
89
+ `Provider "${adapter.id}" does not implement ${method}().`,
90
+ { operation, providerId: adapter.id },
91
+ );
92
+ }
93
+ return adapter[method].bind(adapter);
94
+ }
95
+
96
+ /**
97
+ * Public description of an adapter. Deliberately a fixed shape: the
98
+ * "nothing configured" answer and the "fully configured" answer differ only in
99
+ * array length, never in structure, so modules cannot grow runtime-shaped
100
+ * branches around them.
101
+ */
102
+ function describeAdapter(adapter) {
103
+ return {
104
+ id: adapter.id,
105
+ kind: adapter.kind === ProviderKind.TERRAIN ? "terrain" : "imagery",
106
+ name: adapter.name ?? adapter.id,
107
+ ready: adapter.ready !== false,
108
+ minLevel: adapter.minLevel ?? 0,
109
+ maxLevel: adapter.maxLevel ?? 0,
110
+ tileWidth: adapter.tileWidth ?? 0,
111
+ tileHeight: adapter.tileHeight ?? 0,
112
+ encoding: adapter.encoding ?? 0,
113
+ costClass: adapter.costClass ?? ProviderCost.RESIDENT,
114
+ credit: adapter.credit ?? "",
115
+ fixture: adapter.fixture === true,
116
+ };
117
+ }
118
+
119
+ export function createProviderAccessPort(options = {}) {
120
+ const adapters = new Map();
121
+ const selected = new Map();
122
+ const handles = new Map();
123
+ let nextHandle = 1;
124
+ let lastError = null;
125
+ const stats = {
126
+ acquires: 0,
127
+ reads: 0,
128
+ bytesCopied: 0,
129
+ hostCopies: 0,
130
+ pinned: 0,
131
+ };
132
+
133
+ for (const adapter of options.adapters ?? []) {
134
+ registerAdapter(adapter);
135
+ }
136
+
137
+ function registerAdapter(adapter) {
138
+ if (!adapter || typeof adapter !== "object" || !adapter.id) {
139
+ throw new TypeError("A provider adapter requires an id.");
140
+ }
141
+ const kind = normalizeKind(adapter.kind);
142
+ if (!kind) {
143
+ throw new TypeError(
144
+ `Provider adapter "${adapter.id}" requires kind "terrain" or "imagery".`,
145
+ );
146
+ }
147
+ adapters.set(adapter.id, { ...adapter, kind });
148
+ if (!selected.has(kind)) {
149
+ selected.set(kind, adapter.id);
150
+ }
151
+ }
152
+
153
+ function findAdapter(id, operation) {
154
+ const adapter = adapters.get(id);
155
+ if (!adapter) {
156
+ throw new ProviderAccessError(
157
+ ProviderError.NO_PROVIDER,
158
+ `No provider registered with id "${id}".`,
159
+ { operation, providerId: id },
160
+ );
161
+ }
162
+ return adapter;
163
+ }
164
+
165
+ function resolveAdapter(params, operation) {
166
+ const id = params?.providerId ?? params?.id;
167
+ if (id) return findAdapter(id, operation);
168
+ const kind = normalizeKind(params?.kind);
169
+ const selectedId = kind ? selected.get(kind) : null;
170
+ if (!selectedId) {
171
+ throw new ProviderAccessError(
172
+ ProviderError.NO_PROVIDER,
173
+ kind
174
+ ? `No provider selected for kind "${params?.kind}".`
175
+ : "A providerId or kind is required.",
176
+ { operation },
177
+ );
178
+ }
179
+ return findAdapter(selectedId, operation);
180
+ }
181
+
182
+ /**
183
+ * Normalize what an adapter returned into pinned planes + a descriptor.
184
+ * Adapters return typed arrays; the port never re-encodes them.
185
+ */
186
+ function pin(adapter, result, request, derivedFlags) {
187
+ const planes = (result.planes ?? [result.plane ?? result.data]).map(
188
+ (plane) => {
189
+ if (ArrayBuffer.isView(plane)) {
190
+ return new Uint8Array(
191
+ plane.buffer,
192
+ plane.byteOffset,
193
+ plane.byteLength,
194
+ );
195
+ }
196
+ if (plane instanceof ArrayBuffer) return new Uint8Array(plane);
197
+ throw new ProviderAccessError(
198
+ ProviderError.HOST,
199
+ `Provider "${adapter.id}" returned a non-buffer plane.`,
200
+ { providerId: adapter.id },
201
+ );
202
+ },
203
+ );
204
+ if (planes.length === 0) {
205
+ throw new ProviderAccessError(
206
+ ProviderError.HOST,
207
+ `Provider "${adapter.id}" returned no planes.`,
208
+ { providerId: adapter.id },
209
+ );
210
+ }
211
+
212
+ const info = result.descriptor ?? {};
213
+ const encoding = info.encoding ?? adapter.encoding;
214
+ const layout = encodingLayout(encoding);
215
+ const width = info.width ?? 0;
216
+ const costClass = info.costClass ?? adapter.costClass ?? ProviderCost.RESIDENT;
217
+
218
+ // A result may be dearer than the adapter's declared baseline (a cache
219
+ // miss). Re-check, so the ceiling holds on the actual cost too.
220
+ assertWithinCost(adapter, costClass, request);
221
+
222
+ const descriptor = {
223
+ kind: adapter.kind,
224
+ encoding,
225
+ width,
226
+ height: info.height ?? 1,
227
+ planeCount: planes.length,
228
+ bytesPerElement: layout.bytes,
229
+ rowStrideBytes: info.rowStrideBytes ?? width * layout.bytes,
230
+ byteLength: planes[0].byteLength,
231
+ flags:
232
+ (info.flags ?? 0) |
233
+ derivedFlags |
234
+ (adapter.fixture === true ? ProviderFlags.FIXTURE : 0),
235
+ level: info.level,
236
+ west: info.west,
237
+ south: info.south,
238
+ east: info.east,
239
+ north: info.north,
240
+ minValue: info.minValue,
241
+ maxValue: info.maxValue,
242
+ tileX: info.tileX,
243
+ tileY: info.tileY,
244
+ hostCopies: 1,
245
+ sourceId: providerSourceId(adapter.id),
246
+ costClass,
247
+ strategy: info.strategy ?? 0,
248
+ };
249
+
250
+ const handle = nextHandle++;
251
+ handles.set(handle, { adapter, planes, descriptor });
252
+ stats.acquires += 1;
253
+ stats.pinned = handles.size;
254
+ return { handle, descriptor };
255
+ }
256
+
257
+ const ACQUIRE_METHODS = Object.freeze({
258
+ tile: ["acquireTile", 0],
259
+ profile: ["acquireProfile", ProviderFlags.DERIVED],
260
+ region: ["acquireRegion", ProviderFlags.DERIVED],
261
+ });
262
+
263
+ function acquire(params = {}) {
264
+ const op = String(params.op ?? "tile");
265
+ const selector = ACQUIRE_METHODS[op];
266
+ if (!selector) {
267
+ throw new ProviderAccessError(
268
+ ProviderError.INVALID_REQUEST,
269
+ `Unknown acquire op "${op}".`,
270
+ );
271
+ }
272
+ const [method, derivedFlags] = selector;
273
+ const adapter = resolveAdapter(params, `provider.acquire:${op}`);
274
+ const request = { ...params, maxCost: params.maxCost ?? DEFAULT_MAX_COST };
275
+ const fn = requireAdapterMethod(adapter, method, "provider.acquire");
276
+ // The ceiling is enforced BEFORE the adapter is called. Checking it after
277
+ // the fact would mean a refused acquire had already paid the network or
278
+ // the decode it was refusing — the exact cost the caller declined.
279
+ assertWithinCost(adapter, adapter.costClass ?? ProviderCost.RESIDENT, request);
280
+ return then(fn(request), (result) =>
281
+ pin(adapter, result, request, derivedFlags),
282
+ );
283
+ }
284
+
285
+ function pinnedPlane(handle, plane) {
286
+ const entry = handles.get(handle);
287
+ if (!entry) {
288
+ throw new ProviderAccessError(
289
+ ProviderError.BAD_HANDLE,
290
+ `Unknown or released provider handle ${handle}.`,
291
+ );
292
+ }
293
+ const index = plane | 0;
294
+ if (index < 0 || index >= entry.planes.length) {
295
+ throw new ProviderAccessError(
296
+ ProviderError.BAD_PLANE,
297
+ `Plane ${index} is out of range for handle ${handle} (planeCount ${entry.planes.length}).`,
298
+ );
299
+ }
300
+ return entry.planes[index];
301
+ }
302
+
303
+ /** Bytes for a read, as a VIEW on the pinned buffer — never a copy. */
304
+ function readView(params = {}) {
305
+ const source = pinnedPlane(params.handle, params.plane ?? 0);
306
+ const srcOffset = params.srcOffset | 0;
307
+ if (srcOffset < 0 || srcOffset > source.byteLength) {
308
+ throw new ProviderAccessError(
309
+ ProviderError.BOUNDS,
310
+ `srcOffset ${srcOffset} is outside plane of ${source.byteLength} bytes.`,
311
+ );
312
+ }
313
+ const length = Math.min(
314
+ Math.max(params.length | 0, 0),
315
+ source.byteLength - srcOffset,
316
+ );
317
+ return source.subarray(srcOffset, srcOffset + length);
318
+ }
319
+
320
+ function release(handle) {
321
+ if (!handles.delete(handle)) {
322
+ throw new ProviderAccessError(
323
+ ProviderError.BAD_HANDLE,
324
+ `Unknown or released provider handle ${handle}.`,
325
+ );
326
+ }
327
+ stats.pinned = handles.size;
328
+ return 0;
329
+ }
330
+
331
+ function recordCopies(bytes, copies) {
332
+ stats.reads += 1;
333
+ stats.bytesCopied += bytes;
334
+ stats.hostCopies += copies;
335
+ }
336
+
337
+ const operations = {
338
+ "provider.list": (params = {}) => {
339
+ const kind = normalizeKind(params.kind);
340
+ const providers = [...adapters.values()]
341
+ .filter((adapter) => !kind || adapter.kind === kind)
342
+ .map(describeAdapter)
343
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
344
+ return { providers };
345
+ },
346
+ "provider.describe": (params = {}) => {
347
+ const adapter = findAdapter(params.id, "provider.describe");
348
+ return {
349
+ ...describeAdapter(adapter),
350
+ attributes: adapter.attributes ?? {},
351
+ };
352
+ },
353
+ "provider.select": (params = {}) => {
354
+ const adapter = findAdapter(params.id, "provider.select");
355
+ const kind = normalizeKind(params.kind) ?? adapter.kind;
356
+ if (adapter.kind !== kind) {
357
+ throw new ProviderAccessError(
358
+ ProviderError.INVALID_REQUEST,
359
+ `Provider "${adapter.id}" is not of kind "${params.kind}".`,
360
+ );
361
+ }
362
+ const selection =
363
+ typeof adapter.select === "function" ? adapter.select(params) : null;
364
+ return then(selection, () => {
365
+ selected.set(kind, adapter.id);
366
+ return { selected: adapter.id };
367
+ });
368
+ },
369
+ "provider.configure": (params = {}) => {
370
+ const adapter = findAdapter(params.id, "provider.configure");
371
+ if (typeof adapter.configure !== "function") {
372
+ return { applied: [], rejected: Object.keys(params.settings ?? {}) };
373
+ }
374
+ return adapter.configure(params.settings ?? {});
375
+ },
376
+ "provider.availability": (params = {}) => {
377
+ const adapter = resolveAdapter(params, "provider.availability");
378
+ if (typeof adapter.availability !== "function") {
379
+ return { available: true, known: false };
380
+ }
381
+ return then(adapter.availability(params), (available) => ({
382
+ available: !!available,
383
+ known: true,
384
+ }));
385
+ },
386
+ "provider.prefetch": (params = {}) => {
387
+ const adapter = resolveAdapter(params, "provider.prefetch");
388
+ if (typeof adapter.prefetch !== "function") {
389
+ // Not an error: a camera-driven engine has no region prefetch, and
390
+ // saying so in a field beats inventing an engine API.
391
+ return { requested: 0, pending: 0, supported: false };
392
+ }
393
+ return then(adapter.prefetch(params), (result) => ({
394
+ supported: true,
395
+ ...result,
396
+ }));
397
+ },
398
+ "provider.await": (params = {}) => {
399
+ const adapter = resolveAdapter(params, "provider.await");
400
+ if (typeof adapter.awaitReady !== "function") {
401
+ return { ready: adapter.ready !== false, pending: 0 };
402
+ }
403
+ return adapter.awaitReady(params);
404
+ },
405
+ "provider.lastError": () =>
406
+ lastError ?? {
407
+ code: 0,
408
+ name: "SDM_PROVIDER_OK",
409
+ message: "",
410
+ operation: null,
411
+ },
412
+ "provider.stats": () => ({ ...stats }),
413
+
414
+ // Internal transport operations. Guests never name these — the three
415
+ // `space_data_provider` imports do.
416
+ "provider.acquire": (params = {}) =>
417
+ then(acquire(params), ({ handle, descriptor }) => ({
418
+ handle,
419
+ descriptor: encodeTileDescriptor(descriptor),
420
+ })),
421
+ "provider.readRaw": (params = {}) => {
422
+ const view = readView(params);
423
+ // The staged transport pays a copy here; the direct transport never
424
+ // calls this operation at all.
425
+ const bytes = new Uint8Array(view.byteLength);
426
+ bytes.set(view);
427
+ recordCopies(bytes.byteLength, 2);
428
+ return { bytes };
429
+ },
430
+ "provider.release": (params = {}) => ({
431
+ released: release(params.handle),
432
+ }),
433
+ };
434
+
435
+ function recordFailure(operation, error) {
436
+ const code = providerErrorCodeOf(error);
437
+ lastError = {
438
+ code,
439
+ name: providerErrorName(code),
440
+ message: error?.message ?? String(error),
441
+ operation: error?.operation ?? operation,
442
+ providerId: error?.providerId ?? null,
443
+ };
444
+ return error;
445
+ }
446
+
447
+ function run(operation, params) {
448
+ const handler = operations[operation];
449
+ if (!handler) {
450
+ throw new ProviderAccessError(
451
+ ProviderError.INVALID_REQUEST,
452
+ `Unknown provider operation "${operation}".`,
453
+ { operation },
454
+ );
455
+ }
456
+ let result;
457
+ try {
458
+ result = handler(params ?? {});
459
+ } catch (error) {
460
+ throw recordFailure(operation, error);
461
+ }
462
+ if (isThenable(result)) {
463
+ return result.then(
464
+ (value) => {
465
+ lastError = null;
466
+ return value;
467
+ },
468
+ (error) => {
469
+ throw recordFailure(operation, error);
470
+ },
471
+ );
472
+ }
473
+ lastError = null;
474
+ return result;
475
+ }
476
+
477
+ /**
478
+ * Synchronous invoke. Used by native hosts, where a host call cannot yield.
479
+ * An adapter that returns a promise here is a defect in THAT adapter, and it
480
+ * is named rather than silently awaited into a different runtime's shape.
481
+ */
482
+ function invokeSync(operation, params = null) {
483
+ const result = run(operation, params);
484
+ if (isThenable(result)) {
485
+ throw new ProviderAccessError(
486
+ ProviderError.HOST,
487
+ `Provider operation "${operation}" is asynchronous and needs a blocking transport.`,
488
+ { operation },
489
+ );
490
+ }
491
+ return result;
492
+ }
493
+
494
+ function invoke(operation, params = null) {
495
+ try {
496
+ return Promise.resolve(run(operation, params));
497
+ } catch (error) {
498
+ return Promise.reject(error);
499
+ }
500
+ }
501
+
502
+ return {
503
+ invoke,
504
+ invokeSync,
505
+ operations: Object.keys(operations),
506
+ registerAdapter,
507
+ hasAdapter: (id) => adapters.has(id),
508
+ /**
509
+ * Direct-write read: copies the pinned plane straight into a caller-owned
510
+ * destination view. ONE copy. Used only where the responder can reach guest
511
+ * memory itself.
512
+ */
513
+ directReadInto(params, destination) {
514
+ const view = readView(params);
515
+ destination.set(view.subarray(0, destination.byteLength));
516
+ const written = Math.min(view.byteLength, destination.byteLength);
517
+ recordCopies(written, 1);
518
+ return written;
519
+ },
520
+ releaseAll() {
521
+ handles.clear();
522
+ stats.pinned = 0;
523
+ },
524
+ get stats() {
525
+ return { ...stats };
526
+ },
527
+ };
528
+ }
529
+
530
+ function getMemoryBuffer(getMemory) {
531
+ const memory = getMemory();
532
+ const buffer = memory?.buffer;
533
+ if (!(buffer instanceof ArrayBuffer || buffer instanceof SharedArrayBuffer)) {
534
+ throw new ProviderAccessError(
535
+ ProviderError.HOST,
536
+ "Provider access requires a WebAssembly.Memory-like object.",
537
+ );
538
+ }
539
+ return buffer;
540
+ }
541
+
542
+ function guestView(getMemory, ptr, len) {
543
+ if (!Number.isInteger(ptr) || ptr < 0 || !Number.isInteger(len) || len < 0) {
544
+ throw new ProviderAccessError(
545
+ ProviderError.BOUNDS,
546
+ "Guest pointer and length must be non-negative integers.",
547
+ );
548
+ }
549
+ const buffer = getMemoryBuffer(getMemory);
550
+ if (ptr + len > buffer.byteLength) {
551
+ throw new ProviderAccessError(
552
+ ProviderError.BOUNDS,
553
+ `Guest range [${ptr}, ${ptr + len}) exceeds linear memory (${buffer.byteLength} bytes).`,
554
+ );
555
+ }
556
+ return new Uint8Array(buffer, ptr, len);
557
+ }
558
+
559
+ /**
560
+ * Guest-facing bridge: the three `space_data_provider` imports.
561
+ *
562
+ * `dispatch(operation, params)` must be the same blocking dispatcher the
563
+ * existing hostcall bridge uses — synchronous in-process on native hosts, and
564
+ * the SAB Atomics.wait channel in a browser worker.
565
+ *
566
+ * `directRead` is optional. When supplied, a read is ONE copy: the responder
567
+ * writes the provider's decoded bytes straight into guest linear memory. When
568
+ * absent, the bridge falls back to the staged route (TWO copies) and marks the
569
+ * descriptor with FLAG_STAGED so the guest can see the difference rather than
570
+ * guess at it. Neither route ever encodes tile bytes into a hostcall envelope.
571
+ */
572
+ export function createProviderAccessBridge(options = {}) {
573
+ const getMemory = options.getMemory;
574
+ const dispatch = options.dispatch;
575
+ if (typeof getMemory !== "function") {
576
+ throw new TypeError("createProviderAccessBridge requires getMemory().");
577
+ }
578
+ if (typeof dispatch !== "function") {
579
+ throw new TypeError("createProviderAccessBridge requires dispatch().");
580
+ }
581
+ const directRead =
582
+ typeof options.directRead === "function" ? options.directRead : null;
583
+ const moduleName = options.moduleName ?? PROVIDER_IMPORT_MODULE;
584
+
585
+ const DESC_HOST_COPIES_OFFSET = 104;
586
+ const DESC_FLAGS_OFFSET = 40;
587
+
588
+ function acquire(reqPtr, reqLen, descPtr) {
589
+ try {
590
+ const request = JSON.parse(
591
+ textDecoder.decode(guestView(getMemory, reqPtr, reqLen)),
592
+ );
593
+ // RASTER IN. A coverage field is 512x512 = 262,144 positions; encoding
594
+ // those as JSON would be a multi-megabyte request string parsed per
595
+ // call, which is a worse cost than the tile read it is asking for. So
596
+ // positions may instead be a pointer to interleaved f64 lon/lat pairs
597
+ // already in guest memory, read with ONE copy — symmetric with the way
598
+ // the results come back.
599
+ if (Number.isInteger(request.positionsPtr)) {
600
+ const count = request.positionsCount | 0;
601
+ if (count < 0) {
602
+ throw new ProviderAccessError(
603
+ ProviderError.INVALID_REQUEST,
604
+ "positionsCount must be a non-negative integer.",
605
+ );
606
+ }
607
+ const bytes = guestView(getMemory, request.positionsPtr, count * 16);
608
+ const pairs = new Float64Array(count * 2);
609
+ new Uint8Array(pairs.buffer).set(bytes);
610
+ request.positionsBuffer = pairs;
611
+ delete request.positionsPtr;
612
+ delete request.positionsCount;
613
+ }
614
+ const result = dispatch("provider.acquire", request);
615
+ const descriptor =
616
+ result?.descriptor instanceof Uint8Array
617
+ ? result.descriptor
618
+ : new Uint8Array(result?.descriptor ?? []);
619
+ if (descriptor.byteLength !== PROVIDER_TILE_DESC_BYTES) {
620
+ throw new ProviderAccessError(
621
+ ProviderError.HOST,
622
+ `Descriptor must be ${PROVIDER_TILE_DESC_BYTES} bytes, got ${descriptor.byteLength}.`,
623
+ );
624
+ }
625
+ // Transport truth is patched in HERE, by the only layer that knows the
626
+ // memory topology. The port that produced the descriptor must not guess.
627
+ const patched = new Uint8Array(descriptor);
628
+ const view = new DataView(patched.buffer);
629
+ if (!directRead) {
630
+ view.setUint32(DESC_HOST_COPIES_OFFSET, 2, true);
631
+ view.setUint32(
632
+ DESC_FLAGS_OFFSET,
633
+ view.getUint32(DESC_FLAGS_OFFSET, true) | ProviderFlags.STAGED,
634
+ true,
635
+ );
636
+ }
637
+ guestView(getMemory, descPtr, PROVIDER_TILE_DESC_BYTES).set(patched);
638
+ return result.handle | 0;
639
+ } catch (error) {
640
+ return providerErrorCodeOf(error);
641
+ }
642
+ }
643
+
644
+ function read(handle, plane, srcOffset, dstPtr, dstLen) {
645
+ try {
646
+ if (directRead) {
647
+ return (
648
+ directRead(
649
+ { handle, plane, srcOffset, length: dstLen },
650
+ guestView(getMemory, dstPtr, dstLen),
651
+ ) | 0
652
+ );
653
+ }
654
+ const result = dispatch("provider.readRaw", {
655
+ handle,
656
+ plane,
657
+ srcOffset,
658
+ length: dstLen,
659
+ });
660
+ const bytes =
661
+ result?.bytes instanceof Uint8Array
662
+ ? result.bytes
663
+ : new Uint8Array(result?.bytes ?? []);
664
+ const destination = guestView(getMemory, dstPtr, dstLen);
665
+ const written = Math.min(bytes.byteLength, dstLen);
666
+ destination.set(bytes.subarray(0, written));
667
+ return written;
668
+ } catch (error) {
669
+ return providerErrorCodeOf(error);
670
+ }
671
+ }
672
+
673
+ function release(handle) {
674
+ try {
675
+ dispatch("provider.release", { handle });
676
+ return 0;
677
+ } catch (error) {
678
+ return providerErrorCodeOf(error);
679
+ }
680
+ }
681
+
682
+ return {
683
+ moduleName,
684
+ imports: { [moduleName]: { acquire, read, release } },
685
+ get hostCopiesPerRead() {
686
+ return directRead ? 1 : 2;
687
+ },
688
+ };
689
+ }
690
+
691
+ /**
692
+ * The always-present refusal port.
693
+ *
694
+ * Bound when a host has no provider access at all. It exists so the three
695
+ * imports LINK in every runtime: a missing import is a link-time divergence,
696
+ * the worst class there is. Every call returns a value, never a trap, and the
697
+ * value is the same one a browser gives for an unknown provider id.
698
+ */
699
+ export function createUnavailableProviderPort(
700
+ code = ProviderError.PORT_UNAVAILABLE,
701
+ ) {
702
+ const detail = {
703
+ code,
704
+ name: providerErrorName(code),
705
+ message: "No provider access port is bound in this runtime.",
706
+ operation: null,
707
+ };
708
+ return {
709
+ invoke(operation) {
710
+ if (operation === "provider.list") return Promise.resolve({ providers: [] });
711
+ if (operation === "provider.lastError") return Promise.resolve(detail);
712
+ if (operation === "provider.stats") {
713
+ return Promise.resolve({
714
+ acquires: 0,
715
+ reads: 0,
716
+ bytesCopied: 0,
717
+ hostCopies: 0,
718
+ pinned: 0,
719
+ });
720
+ }
721
+ return Promise.reject(
722
+ new ProviderAccessError(code, detail.message, { operation }),
723
+ );
724
+ },
725
+ operations: ["provider.list", "provider.lastError", "provider.stats"],
726
+ };
727
+ }