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.
- package/README.md +13 -0
- package/docs/AGENTS.md +5 -0
- package/docs/browser-wasmedge-isomorphic.md +65 -1
- package/docs/language-runtime-matrix.md +2 -2
- package/docs/provider-access-abi.md +600 -0
- package/package.json +15 -6
- package/schemas/orbpro/Propagator.fbs +19 -3
- package/src/browser.js +14 -4
- package/src/bundle/artifactBytes.js +44 -0
- package/src/bundle/index.js +1 -0
- package/src/compiler/compileModule.js +24 -1
- package/src/flow/flowCompiler.js +141 -2
- package/src/flow/flowRuntimeHost.js +7 -1
- package/src/flow/isomorphicFlowHost.js +1 -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/{testing → host}/browserModuleHarness.js +69 -34
- package/src/host/index.js +6 -0
- package/src/host/isomorphicLoader.js +17 -45
- package/src/host/isomorphicLoaderBrowser.js +47 -0
- package/src/host/isomorphicLoaderCore.js +58 -0
- package/src/host/nodeBuiltinSpecifier.js +43 -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/sabHostcallChannel.js +1 -1
- package/src/host/terrainSourceSeam.js +205 -0
- package/src/host/wasiThreadHost.js +11 -1
- package/src/{testing → host}/workerModuleHarness.js +45 -12
- package/src/{testing → host}/workerModuleHarnessWorker.js +6 -5
- package/src/index.d.ts +29 -2
- package/src/standards/browser.js +95 -0
- package/src/standards/catalogCore.js +290 -0
- package/src/standards/index.js +27 -251
- package/src/testing/AGENTS.md +25 -2
- package/src/testing/browser.js +32 -0
- package/src/testing/index.d.ts +12 -1
- package/src/testing/index.js +3 -3
- package/src/testing/parityBrowserRunner.js +9 -2
- package/src/testing/parityHarness.js +1 -1
- package/templates/provider-access-module/include/space_data_provider_abi.h +227 -0
- /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
|
+
}
|