@bitruvius/foundation 0.3.0
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/LICENSE +167 -0
- package/README.md +76 -0
- package/dist/capability/detect.d.ts +62 -0
- package/dist/errors.d.ts +84 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +1 -0
- package/dist/scheduling/abort-error.d.ts +20 -0
- package/dist/scheduling/cdn-worker-trampoline.d.ts +58 -0
- package/dist/scheduling/fixtures/echo.worker.d.ts +5 -0
- package/dist/scheduling/task-queue.d.ts +21 -0
- package/dist/scheduling/worker-degradation.d.ts +47 -0
- package/dist/scheduling/worker-pool.d.ts +97 -0
- package/dist/sdk-config.d.ts +111 -0
- package/dist/transport/authorizer.d.ts +114 -0
- package/dist/transport/fetch-asset.d.ts +39 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Bitruvius SDK License Agreement
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bitruvius, Inc. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This Bitruvius SDK License Agreement (the "Agreement") is between you and
|
|
6
|
+
Bitruvius, Inc., a Delaware corporation ("Bitruvius"), and governs the use of
|
|
7
|
+
the Bitruvius SDK, including all @bitruvius npm packages, browser bundles,
|
|
8
|
+
WebAssembly binaries, documentation, and related materials distributed with
|
|
9
|
+
this notice (collectively, the "SDK").
|
|
10
|
+
|
|
11
|
+
1. LICENSE GRANT
|
|
12
|
+
|
|
13
|
+
Subject to the terms of this Agreement, Bitruvius grants you a worldwide,
|
|
14
|
+
royalty-free, non-exclusive, non-transferable, revocable license to:
|
|
15
|
+
|
|
16
|
+
(a) use the SDK, in unmodified form, in any application, website, or
|
|
17
|
+
service you develop, whether commercial or non-commercial (an
|
|
18
|
+
"Application"); and
|
|
19
|
+
(b) reproduce and distribute the SDK solely as embedded in or bundled
|
|
20
|
+
with an Application (for example, as part of a compiled or bundled
|
|
21
|
+
build of the Application), provided the SDK is not distributed on a
|
|
22
|
+
standalone basis and remains subject to this Agreement.
|
|
23
|
+
|
|
24
|
+
2. RESTRICTIONS
|
|
25
|
+
|
|
26
|
+
Except as expressly permitted in Section 1 or by applicable law
|
|
27
|
+
notwithstanding this limitation, you may not, and may not permit or assist
|
|
28
|
+
any third party to:
|
|
29
|
+
|
|
30
|
+
(a) modify, adapt, translate, or create derivative works of the SDK;
|
|
31
|
+
(b) redistribute, publish, sublicense, sell, rent, lease, or otherwise
|
|
32
|
+
make the SDK available on a standalone basis, including republishing
|
|
33
|
+
the SDK or any portion of it to any package registry or content
|
|
34
|
+
delivery network;
|
|
35
|
+
(c) reverse engineer, decompile, disassemble, decode, or otherwise
|
|
36
|
+
attempt to derive or reconstruct the source code, underlying ideas,
|
|
37
|
+
algorithms, file formats, or non-public APIs of the SDK, including
|
|
38
|
+
the WebAssembly binaries contained in the SDK;
|
|
39
|
+
(d) use the SDK, or any output or knowledge derived from it, to develop,
|
|
40
|
+
train, or improve a product or service that competes with the SDK or
|
|
41
|
+
with Bitruvius codec technology, including implementing or
|
|
42
|
+
reimplementing encoders or decoders for Bitruvius codec formats;
|
|
43
|
+
(e) remove, obscure, or alter any copyright, license, or attribution
|
|
44
|
+
notices contained in the SDK.
|
|
45
|
+
|
|
46
|
+
THIRD-PARTY COMPONENTS. Nothing in this Section 2 limits any right granted to
|
|
47
|
+
you directly by a third party under the license covering a third-party
|
|
48
|
+
component distributed with the SDK. Those components and their licenses are
|
|
49
|
+
identified in the NOTICES files distributed alongside them, together with a
|
|
50
|
+
copy of each license. Where this Agreement conflicts with such a license as
|
|
51
|
+
applied to that component, the third-party license governs that component.
|
|
52
|
+
|
|
53
|
+
THIRD-PARTY DATA AND SERVICES. The SDK is able to connect to data, imagery,
|
|
54
|
+
tilesets, and services operated by third parties, some of which require
|
|
55
|
+
credentials that you obtain in your own name. This Agreement grants you no
|
|
56
|
+
right, title, or license in or to any such data or service. Your access to and
|
|
57
|
+
use of them is governed solely by your agreement with the relevant provider,
|
|
58
|
+
including any obligation to display attribution or to restrict redistribution.
|
|
59
|
+
Bitruvius does not supply, endorse, or warrant any third-party data or service,
|
|
60
|
+
is not a party to your agreement with any provider, and bears no responsibility
|
|
61
|
+
for its availability, accuracy, terms, or charges.
|
|
62
|
+
|
|
63
|
+
3. OWNERSHIP
|
|
64
|
+
|
|
65
|
+
The SDK is licensed, not sold. Bitruvius and its licensors retain all
|
|
66
|
+
right, title, and interest in and to the SDK, including all intellectual
|
|
67
|
+
property rights. No rights are granted except as expressly set out in this
|
|
68
|
+
Agreement.
|
|
69
|
+
|
|
70
|
+
4. PRERELEASE VERSIONS
|
|
71
|
+
|
|
72
|
+
Versions of the SDK identified as alpha or beta (including any 0.x or
|
|
73
|
+
prerelease-tagged versions) are provided for evaluation and testing,
|
|
74
|
+
may change or be discontinued at any time without notice, and are provided
|
|
75
|
+
without any commitment of support or backward compatibility.
|
|
76
|
+
|
|
77
|
+
5. NO WARRANTY
|
|
78
|
+
|
|
79
|
+
THE SDK IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT WARRANTY OF ANY
|
|
80
|
+
KIND, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY WARRANTIES OF
|
|
81
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, OR
|
|
82
|
+
NON-INFRINGEMENT. BITRUVIUS DOES NOT WARRANT THAT THE SDK WILL BE
|
|
83
|
+
UNINTERRUPTED, ERROR-FREE, OR SECURE.
|
|
84
|
+
|
|
85
|
+
6. LIMITATION OF LIABILITY
|
|
86
|
+
|
|
87
|
+
TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT WILL BITRUVIUS BE
|
|
88
|
+
LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR EXEMPLARY
|
|
89
|
+
DAMAGES, OR FOR ANY LOSS OF PROFITS, REVENUE, DATA, OR GOODWILL, ARISING
|
|
90
|
+
OUT OF OR RELATING TO THIS AGREEMENT OR THE SDK, EVEN IF ADVISED OF THE
|
|
91
|
+
POSSIBILITY OF SUCH DAMAGES. BITRUVIUS'S TOTAL AGGREGATE LIABILITY ARISING
|
|
92
|
+
OUT OF OR RELATING TO THIS AGREEMENT WILL NOT EXCEED ONE HUNDRED (100) US
|
|
93
|
+
DOLLARS.
|
|
94
|
+
|
|
95
|
+
7. INDEMNIFICATION
|
|
96
|
+
|
|
97
|
+
You will defend, indemnify, and hold harmless Bitruvius and its officers,
|
|
98
|
+
directors, employees, and agents from and against any third-party claim,
|
|
99
|
+
demand, suit, or proceeding, and any resulting losses, damages, liabilities,
|
|
100
|
+
settlements, costs, and reasonable attorneys' fees, arising out of or
|
|
101
|
+
relating to: (a) your Applications or any product or service you build with
|
|
102
|
+
or distribute using the SDK; (b) your data, content, or the services you
|
|
103
|
+
direct the SDK to access; (c) your use of the SDK in violation of this
|
|
104
|
+
Agreement or of any applicable law; or (d) your infringement or
|
|
105
|
+
misappropriation of any third party's rights. Bitruvius will notify you of
|
|
106
|
+
any such claim, allow you to control the defense (except that any settlement
|
|
107
|
+
requiring an admission or payment by Bitruvius requires its consent, not to
|
|
108
|
+
be unreasonably withheld), and provide reasonable cooperation at your
|
|
109
|
+
expense.
|
|
110
|
+
|
|
111
|
+
BITRUVIUS PROVIDES NO INDEMNITY OF ANY KIND UNDER THIS AGREEMENT, INCLUDING
|
|
112
|
+
NO DEFENSE OR INDEMNITY AGAINST ANY CLAIM THAT THE SDK INFRINGES OR
|
|
113
|
+
MISAPPROPRIATES ANY PATENT, COPYRIGHT, TRADEMARK, TRADE SECRET, OR OTHER
|
|
114
|
+
INTELLECTUAL PROPERTY RIGHT. THE SDK IS LICENSED AT NO CHARGE AND IS
|
|
115
|
+
PROVIDED WITHOUT SUCH PROTECTION. If you require an intellectual property
|
|
116
|
+
indemnity, contact legal@bitruvius.com about a commercial license; any such
|
|
117
|
+
indemnity applies only if expressly agreed in a signed written agreement.
|
|
118
|
+
|
|
119
|
+
8. TERMINATION
|
|
120
|
+
|
|
121
|
+
This Agreement is effective until terminated. It terminates automatically,
|
|
122
|
+
without notice, if you breach any of its terms. Upon termination you must
|
|
123
|
+
cease all use of the SDK and destroy all copies in your possession or
|
|
124
|
+
control, except that Applications already distributed to end users with the
|
|
125
|
+
SDK embedded may continue to be used. Sections 2, 3, 5, 6, 7, 8, and 9
|
|
126
|
+
survive termination.
|
|
127
|
+
|
|
128
|
+
9. GENERAL
|
|
129
|
+
|
|
130
|
+
This Agreement constitutes the entire agreement between you and Bitruvius
|
|
131
|
+
concerning the SDK and supersedes any prior agreements. Bitruvius may
|
|
132
|
+
publish updated or supplemental terms for future SDK versions at
|
|
133
|
+
https://bitruvius.com/eula; the terms accompanying a given SDK version
|
|
134
|
+
govern that version. If any provision of this Agreement is held
|
|
135
|
+
unenforceable, the remaining provisions remain in full force.
|
|
136
|
+
|
|
137
|
+
GOVERNING LAW AND VENUE. This Agreement is governed by and construed in
|
|
138
|
+
accordance with the laws of the State of Georgia and the applicable federal
|
|
139
|
+
law of the United States, without regard to conflict-of-law provisions, and
|
|
140
|
+
excluding the United Nations Convention on Contracts for the International
|
|
141
|
+
Sale of Goods. The exclusive venue for any dispute arising out of or
|
|
142
|
+
relating to this Agreement or the SDK is the state or federal courts located
|
|
143
|
+
in Cobb County, Georgia, and each party consents to personal jurisdiction
|
|
144
|
+
and venue in those courts.
|
|
145
|
+
|
|
146
|
+
ASSIGNMENT. You may not assign or transfer this Agreement, in whole or in
|
|
147
|
+
part, without Bitruvius's prior written consent, and any attempted
|
|
148
|
+
assignment without that consent is void. Bitruvius may assign this
|
|
149
|
+
Agreement freely, including in connection with a merger, acquisition, or
|
|
150
|
+
sale of assets. This Agreement binds and benefits the parties' permitted
|
|
151
|
+
successors and assigns.
|
|
152
|
+
|
|
153
|
+
EXPORT COMPLIANCE. The SDK may be subject to export control laws,
|
|
154
|
+
including the U.S. Export Administration Regulations and applicable
|
|
155
|
+
sanctions programs. You represent that you are not located in, and will
|
|
156
|
+
not use or export the SDK in or to, any country or to any person or entity
|
|
157
|
+
subject to such restrictions, and that you will comply with all applicable
|
|
158
|
+
export and import laws.
|
|
159
|
+
|
|
160
|
+
U.S. GOVERNMENT END USERS. The SDK is "commercial computer software" and
|
|
161
|
+
"commercial computer software documentation" as those terms are used in
|
|
162
|
+
48 C.F.R. 12.212 and 48 C.F.R. 227.7202. Any use, duplication, or
|
|
163
|
+
disclosure by or on behalf of the U.S. Government is governed solely by
|
|
164
|
+
the terms of this Agreement.
|
|
165
|
+
|
|
166
|
+
For commercial licensing of Bitruvius encoder technology, or any questions
|
|
167
|
+
about this Agreement, contact: legal@bitruvius.com
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://bitruvius.com"><img src="https://cdn.bitruvius.com/brand/bitruvius-logo.png" width="360" alt="Bitruvius"></a>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# @bitruvius/foundation
|
|
6
|
+
|
|
7
|
+
**The dependency root of the Bitruvius SDK: scheduling, capability detection, transport authorization, configuration and errors, with nothing underneath it.**
|
|
8
|
+
|
|
9
|
+
> **Internal building block.** This package exists so that
|
|
10
|
+
> [`@bitruvius/sdk-maplibre`](https://www.npmjs.com/package/@bitruvius/sdk-maplibre) and the
|
|
11
|
+
> Bitruvius codecs can resolve their dependencies on npm. It has no standalone product story.
|
|
12
|
+
> Unless you are deliberately building against it, install the SDK instead.
|
|
13
|
+
|
|
14
|
+
## Why it is a separate package
|
|
15
|
+
|
|
16
|
+
Bitruvius ships as many small packages: a codec per format, a geospatial layer, an auth adapter, a renderer SDK. They all need the same worker pool, the same error type and the same configuration store, and none of them may depend on each other. A raster codec cannot depend on the geospatial layer, and the geospatial layer cannot depend on a raster codec.
|
|
17
|
+
|
|
18
|
+
So the shared pieces moved down to the one package with nothing beneath it. This package has zero runtime dependencies and touches no WebGL, DOM, wasm or geospatial code. That is what lets `@bitruvius/ript`, a scientific raster codec with no interest in maps, take the same worker pool as `@bitruvius/sdk-maplibre`. The geospatial layer is `@bitruvius/geo-core`, which builds on this.
|
|
19
|
+
|
|
20
|
+
Two more things force a single low-level home:
|
|
21
|
+
|
|
22
|
+
**Configuration has to be shared across copies of the SDK.** `configure()` writes to a namespaced key on `globalThis`, not to a module-scoped variable. A page can load several per-package CDN bundles, each carrying its own copy of the SDK, and they must still see one `wasmBaseUrl` and one signed-in ArcGIS session.
|
|
23
|
+
|
|
24
|
+
**Errors have to survive duplicate bundles.** `BitruviusError.is()` duck-types on shape instead of using `instanceof`, for the same reason: two copies of the SDK on one page each throw errors that fail the other's `instanceof`. The stable `BVX_*` code set is defined exactly once, here, so every package throws identifiers you can still branch on after minification.
|
|
25
|
+
|
|
26
|
+
## What is in it
|
|
27
|
+
|
|
28
|
+
**Scheduling.** `WorkerPool` is a transferable Web Worker pool with per-task `AbortSignal` handling, a warm-up handshake, `broadcast`, and self-healing respawn under a budget, so a broken worker module or wasm URL fails the pool loudly rather than hanging every tile. `TaskQueue` bounds concurrency. `createCdnWorkerTrampoline` solves the cross-origin worker problem: `new Worker(crossOriginUrl)` throws a synchronous `SecurityError` before any network request is made, so a CDN-hosted SDK fetches its worker chunk's text and spawns it from a same-origin `blob:` URL instead. `reportWorkerDegradation` makes the main-thread fallback audible once per label, because a layer that renders slowly and a layer that renders nothing were both shipping silently.
|
|
29
|
+
|
|
30
|
+
**Capability detection.** `detectCapabilities()` probes WebGL2, WebGPU, Workers, transferables and WebAssembly SIMD. SIMD is a hard gate rather than a preference: the codec wasm is compiled with `+simd128` and there is no non-SIMD build, so `recommendDecodeBackend()` throws instead of pretending. `DecodeBackend` names the execution model a decoder will use.
|
|
31
|
+
|
|
32
|
+
**Transport.** `RequestAuthorizer` is a drop-in replacement for `fetch`, scoped to one streaming session. It is a `fetch` override rather than an `authorize()` callback because every call site already calls `fetch`, and because only the override holds the `Response`, which is what lets `@bitruvius/esri-auth` detect the ArcGIS challenge that arrives as HTTP 200 with `{ error: { code: 498 } }` in the body.
|
|
33
|
+
|
|
34
|
+
`publicAuthorizer()` also enforces a credential trust boundary. A tileset is a third-party document that names further URLs, and the streaming engines fetch every one of them. Without the boundary, a tileset naming `https://attacker.example/tile.glb` harvests a Cesium ion token or a Google API key, both reusable and both billable, delivered by our own transport. Credentials go to the origin they were minted for and to nowhere else; other hosts get the request without them, and are warned once. A tile that fails to load is recoverable in a way that a leaked key is not.
|
|
35
|
+
|
|
36
|
+
`fetchBinaryAsset()` fails on the transport problem instead of handing a wrong-typed body to a codec. A missing file that 404s to an HTML fallback page otherwise surfaces as a bad magic number of `1868833084`, which is the ASCII `<!do` of `<!doctype html>`, and sends you off to debug a decoder that is working correctly.
|
|
37
|
+
|
|
38
|
+
**Configuration.** `configure`, `getConfig`, `configuredWasmUrl` and `resolveAuth`. One call points every codec at a wasm folder; an explicit per-decoder `wasmUrl` or `wasmBytes` still wins over it.
|
|
39
|
+
|
|
40
|
+
**Errors.** `BitruviusError`, `BvxCode`, `BVX_CODES` and `asBitruviusError`, plus `abortError` and `isAbortError`, which follow the WHATWG cancellation convention with a plain `Error` named `AbortError` rather than a `DOMException`, since that is not constructible in every worker or test realm.
|
|
41
|
+
|
|
42
|
+
## Depending on it directly
|
|
43
|
+
|
|
44
|
+
Inside the SDK, `@bitruvius/geo-core`, `@bitruvius/codec-runtime`, `@bitruvius/esri-auth`, `@bitruvius/sdk-maplibre` and the codec packages all depend on it, which is why it is published at all.
|
|
45
|
+
|
|
46
|
+
Outside the SDK, reach for it in two cases: you are writing a custom `RequestAuthorizer`, or you are catching SDK errors and want the error type and code list at their source. Both are re-exported by `@bitruvius/sdk-maplibre`, so installing this package on its own is rarely the answer.
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npm i @bitruvius/foundation
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { BitruviusError, configure } from '@bitruvius/foundation';
|
|
54
|
+
|
|
55
|
+
// Serve every codec's .wasm from one folder (note the trailing slash).
|
|
56
|
+
configure({ wasmBaseUrl: 'https://my.app/assets/bitruvius/' });
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await layer.load();
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (BitruviusError.is(e, 'BVX_GL_UNAVAILABLE')) showWebGl2Warning();
|
|
62
|
+
else throw e;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Trademarks
|
|
67
|
+
|
|
68
|
+
Esri and ArcGIS are trademarks of Environmental Systems Research Institute, Inc. Cesium is a trademark of Cesium GS, Inc. Google is a trademark of Google LLC. MapLibre is a trademark of the MapLibre organization. All other marks are the property of their respective owners.
|
|
69
|
+
|
|
70
|
+
These names are used solely to describe the data formats this software interoperates with. WebGL is a trademark of The Khronos Group Inc.
|
|
71
|
+
|
|
72
|
+
Bitruvius is not affiliated with, sponsored by, or endorsed by any of them, and no such relationship is implied.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
See [LICENSE](./LICENSE). © Bitruvius, Inc.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime capability detection + decode-backend recommendation.
|
|
3
|
+
*
|
|
4
|
+
* The vendored BVC wasm is compiled with `+simd128`, so it can only run
|
|
5
|
+
* where WASM SIMD is available — `wasmSimd` is therefore a hard
|
|
6
|
+
* requirement, not a nice-to-have. The renderer needs WebGL2.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* How the bytes→arrays decode work is performed. P1 ships `cpu-wasm-workers`;
|
|
10
|
+
* the rest are reserved for fast-follow backends. This is a decode-**execution**
|
|
11
|
+
* concern, so it lives in the runtime base (not the geospatial layer).
|
|
12
|
+
*/
|
|
13
|
+
export type DecodeBackend = 'auto' | 'cpu-wasm-workers' | 'webgl2-unpack' | 'webgpu-compute' | 'cpu-wasm';
|
|
14
|
+
/** The runtime features the SDK probes via {@link detectCapabilities}. */
|
|
15
|
+
export interface Capabilities {
|
|
16
|
+
/** WebGL2 is available (required to render). */
|
|
17
|
+
readonly webgl2: boolean;
|
|
18
|
+
/** WebGPU is available (reserved for future GPU-decode backends). */
|
|
19
|
+
readonly webgpu: boolean;
|
|
20
|
+
/** Web Workers are available (enables off-main-thread decode). */
|
|
21
|
+
readonly workers: boolean;
|
|
22
|
+
/** Transferable objects (`MessageChannel`) are available (zero-copy worker handoff). */
|
|
23
|
+
readonly transferable: boolean;
|
|
24
|
+
/** WebAssembly fixed-width SIMD is available (REQUIRED — the codecs are `+simd128`). */
|
|
25
|
+
readonly wasmSimd: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** The subset of globals {@link detectCapabilities} probes; pass a partial object in
|
|
28
|
+
* tests to inject a deterministic environment. Defaults to `globalThis`. */
|
|
29
|
+
export interface DetectEnv {
|
|
30
|
+
/** The `Worker` constructor, if present. */
|
|
31
|
+
Worker?: unknown;
|
|
32
|
+
/** The `MessageChannel` constructor, if present. */
|
|
33
|
+
MessageChannel?: unknown;
|
|
34
|
+
/** The `WebGL2RenderingContext` global, if present. */
|
|
35
|
+
WebGL2RenderingContext?: unknown;
|
|
36
|
+
/** `navigator`, probed for `navigator.gpu`. */
|
|
37
|
+
navigator?: {
|
|
38
|
+
gpu?: unknown;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Probe whether the runtime supports WebAssembly fixed-width SIMD (validates a tiny
|
|
42
|
+
* `v128` module). @returns `true` when SIMD is supported. */
|
|
43
|
+
export declare function isWasmSimdSupported(): boolean;
|
|
44
|
+
/** Detect the SDK-relevant runtime {@link Capabilities}.
|
|
45
|
+
* @param env - Globals to probe (default `globalThis`; override in tests).
|
|
46
|
+
* @returns The detected {@link Capabilities}.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const caps = detectCapabilities();
|
|
51
|
+
* if (!caps.webgl2 || !caps.wasmSimd) showUnsupportedBrowserNotice();
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function detectCapabilities(env?: DetectEnv): Capabilities;
|
|
55
|
+
/**
|
|
56
|
+
* Recommend the P1 {@link DecodeBackend} for the given capabilities.
|
|
57
|
+
* @param caps - At least `workers` and `wasmSimd` from {@link detectCapabilities}.
|
|
58
|
+
* @returns `'cpu-wasm-workers'` when Workers are available, else `'cpu-wasm'`.
|
|
59
|
+
* @throws Error when `wasmSimd` is `false` — there is no non-SIMD wasm build, so decode
|
|
60
|
+
* cannot proceed in that runtime.
|
|
61
|
+
*/
|
|
62
|
+
export declare function recommendDecodeBackend(caps: Pick<Capabilities, 'workers' | 'wasmSimd'>): DecodeBackend;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stable set of Bitruvius error codes. Append-only and never renamed, so it is the
|
|
3
|
+
* one dependable error API across the SDK: branch on a {@link BitruviusError.code},
|
|
4
|
+
* not on message text or class names (shipped JS is minified).
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Each entry is a `BVX_*` string; the union of them is {@link BvxCode}. Use
|
|
8
|
+
* {@link BitruviusError.is} to test a caught value against one of these.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { BitruviusError } from '@bitruvius/foundation';
|
|
13
|
+
*
|
|
14
|
+
* try {
|
|
15
|
+
* await decoder.decode(bytes);
|
|
16
|
+
* } catch (e) {
|
|
17
|
+
* if (BitruviusError.is(e, 'BVX_WASM_LOAD_FAILED')) {
|
|
18
|
+
* // CSP / MIME / network: fall back to a configured wasm URL, or surface a hint.
|
|
19
|
+
* } else {
|
|
20
|
+
* throw e;
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare const BVX_CODES: readonly ["BVX_WASM_LOAD_FAILED", "BVX_DECODE_FAILED", "BVX_NO_BACKEND", "BVX_WORKER_SPAWN_BLOCKED", "BVX_GL_UNAVAILABLE", "BVX_BAD_INPUT", "BVX_ASSET_FETCH_FAILED"];
|
|
26
|
+
/** One of the {@link BVX_CODES} — the stable, minification-proof error identifier. */
|
|
27
|
+
export type BvxCode = (typeof BVX_CODES)[number];
|
|
28
|
+
/**
|
|
29
|
+
* The SDK's error type. Every error the SDK throws on purpose is a `BitruviusError`
|
|
30
|
+
* carrying a stable {@link BvxCode}; consumers branch on `err.code` and support triages
|
|
31
|
+
* from it. The `message` is `"<code>: <detail>"`.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* Prefer the {@link BitruviusError.is} type-guard over `instanceof` — it is
|
|
35
|
+
* cross-realm / duplicate-bundle safe (two SDK copies on one page can each throw a
|
|
36
|
+
* `BitruviusError` that fails the other's `instanceof`).
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { BitruviusError } from '@bitruvius/foundation';
|
|
41
|
+
*
|
|
42
|
+
* try {
|
|
43
|
+
* await layer.load();
|
|
44
|
+
* } catch (e) {
|
|
45
|
+
* if (BitruviusError.is(e, 'BVX_GL_UNAVAILABLE')) showWebGl2Warning();
|
|
46
|
+
* else throw e;
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare class BitruviusError extends Error {
|
|
51
|
+
readonly name = "BitruviusError";
|
|
52
|
+
/** The stable {@link BvxCode} for this error — the value to branch on. */
|
|
53
|
+
readonly code: BvxCode;
|
|
54
|
+
/**
|
|
55
|
+
* @param code - The stable {@link BvxCode} for this failure.
|
|
56
|
+
* @param message - Human-readable detail; prefixed with `<code>: ` in `.message`.
|
|
57
|
+
* @param options - Standard error options; `cause` preserves the underlying error.
|
|
58
|
+
*/
|
|
59
|
+
constructor(code: BvxCode, message: string, options?: {
|
|
60
|
+
cause?: unknown;
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* Type-guard: is `err` a {@link BitruviusError} (optionally with a specific
|
|
64
|
+
* {@link BvxCode})? Duck-types on shape (not `instanceof`), so it is safe across
|
|
65
|
+
* realms and duplicate SDK bundles.
|
|
66
|
+
*
|
|
67
|
+
* @param err - The caught value to test.
|
|
68
|
+
* @param code - When given, also require `err.code === code`.
|
|
69
|
+
* @returns `true` (narrowing `err` to {@link BitruviusError}) when `err` is a
|
|
70
|
+
* Bitruvius error and, if `code` was passed, carries that exact code.
|
|
71
|
+
*/
|
|
72
|
+
static is(err: unknown, code?: BvxCode): err is BitruviusError;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Wrap an unknown thrown value as a {@link BitruviusError}, preserving the cause. If
|
|
76
|
+
* `cause` is already a `BitruviusError` it is returned unchanged (the original code is
|
|
77
|
+
* kept), so re-wrapping at multiple layers never loses the first/most-specific code.
|
|
78
|
+
*
|
|
79
|
+
* @param code - The {@link BvxCode} to use when `cause` is not already a `BitruviusError`.
|
|
80
|
+
* @param message - Human-readable detail for the wrapping error.
|
|
81
|
+
* @param cause - The original thrown value, preserved as `.cause`.
|
|
82
|
+
* @returns A {@link BitruviusError} — `cause` itself when it already was one.
|
|
83
|
+
*/
|
|
84
|
+
export declare function asBitruviusError(code: BvxCode, message: string, cause: unknown): BitruviusError;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bitruvius/foundation — the domain-agnostic foundation shared by every Bitruvius SDK.
|
|
3
|
+
*
|
|
4
|
+
* Pure-TS, geo-free building blocks: off-main-thread scheduling, runtime capability
|
|
5
|
+
* detection (+ the `DecodeBackend` execution model), the request-authorization seam,
|
|
6
|
+
* SDK configuration (e.g. the wasm base URL), and the shared error type. No WebGL, DOM,
|
|
7
|
+
* wasm, or geospatial concerns live here — the geospatial layer is `@bitruvius/geo-core`,
|
|
8
|
+
* which builds on this package.
|
|
9
|
+
*/
|
|
10
|
+
export * from './capability/detect';
|
|
11
|
+
export * from './scheduling/abort-error';
|
|
12
|
+
export * from './scheduling/task-queue';
|
|
13
|
+
export * from './scheduling/worker-pool';
|
|
14
|
+
export * from './scheduling/cdn-worker-trampoline';
|
|
15
|
+
export * from './scheduling/worker-degradation';
|
|
16
|
+
export * from './transport/authorizer';
|
|
17
|
+
export * from './transport/fetch-asset';
|
|
18
|
+
export * from './sdk-config';
|
|
19
|
+
export * from './errors';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var A=new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11]);function L(){try{return typeof WebAssembly<"u"&&WebAssembly.validate(A)}catch{return!1}}function k(G=globalThis){return{webgl2:typeof G.WebGL2RenderingContext<"u",webgpu:!!G.navigator&&"gpu"in G.navigator&&G.navigator.gpu!=null,workers:typeof G.Worker<"u",transferable:typeof G.MessageChannel<"u",wasmSimd:L()}}function m(G){if(!G.wasmSimd)throw Error("BVC decode requires WebAssembly SIMD (the vendored wasm is built with +simd128); this runtime lacks it.");return G.workers?"cpu-wasm-workers":"cpu-wasm"}function W(G){let J=G instanceof Error?G.message:G!==void 0?String(G):"The operation was aborted",Q=Error(J);return Q.name="AbortError",Q}function p(G){return(G instanceof Error||typeof DOMException<"u"&&G instanceof DOMException)&&G.name==="AbortError"}class U{concurrency;active=0;pending=[];idleWaiters=[];constructor(G){this.concurrency=G;if(G<1)throw Error("TaskQueue concurrency must be >= 1")}get size(){return this.active+this.pending.length}run(G){return new Promise((J,Q)=>{let Z=()=>{this.active++,G().then(J,Q).finally(()=>this.afterTask())};if(this.active<this.concurrency)Z();else this.pending.push(Z)})}onIdle(){if(this.size===0)return Promise.resolve();return new Promise((G)=>this.idleWaiters.push(G))}afterTask(){this.active--;let G=this.pending.shift();if(G)G();else if(this.size===0){let J=this.idleWaiters.splice(0);for(let Q of J)Q()}}}class _{workers=[];idle=[];queue=[];inflight=new Map;byWorker=new Map;broadcasts=new Map;nextId=1;disposed=!1;spawn;targetSize;respawns=0;broken=null;warmup;onDiscard;warmPending=0;warmWaiters=[];constructor(G){if(G.size<1)throw Error("WorkerPool size must be >= 1");if(this.spawn=G.spawn,this.targetSize=G.size,G.warmup)this.warmup=G.warmup;if(G.onDiscard)this.onDiscard=G.onDiscard;for(let J=0;J<G.size;J++)this.addWorker()}addWorker(){let G=this.spawn();if(G.onmessage=($)=>this.onMessage(G,$),G.onerror=($)=>this.onError(G,$),this.workers.push(G),!this.warmup){this.idle.push(G);return}let J;try{J=this.warmup()}catch{this.replaceWorker(G);return}this.warmPending++;let Q=this.nextId++,Z={request:J.request,transfer:J.transfer??[],resolve:()=>this.noteWarm(),reject:()=>{if(this.workers.includes(G))this.replaceWorker(G);this.noteWarm()}};this.inflight.set(Q,{job:Z,worker:G}),this.byWorker.set(G,Q);try{G.postMessage({__id:Q,request:J.request},J.transfer??[])}catch($){this.inflight.delete(Q),this.byWorker.delete(G),this.rejectJob(Z,$)}}noteWarm(){if(--this.warmPending>0)return;this.warmPending=0;let G=this.warmWaiters.splice(0);for(let J of G)J()}whenWarm(){if(this.warmPending===0)return Promise.resolve();return new Promise((G)=>this.warmWaiters.push(G))}get size(){return this.queue.length+this.inflight.size}get queueLength(){return this.queue.length}get inflightCount(){return this.inflight.size}run(G,J=[],Q){if(this.disposed)return Promise.reject(Error("WorkerPool disposed"));if(this.broken)return Promise.reject(this.broken);let Z=Q?.signal;if(Z?.aborted)return Promise.reject(W(Z.reason));return new Promise(($,V)=>{let z={request:G,transfer:J,resolve:$,reject:V};if(Z)z.signal=Z,z.onAbort=()=>{let X=this.queue.indexOf(z);if(X>=0)this.queue.splice(X,1);this.rejectJob(z,W(Z.reason))},Z.addEventListener("abort",z.onAbort,{once:!0});this.queue.push(z),this.pump()})}broadcast(G,J=[]){if(this.disposed)return Promise.reject(Error("WorkerPool disposed"));if(this.broken)return Promise.reject(this.broken);return Promise.all([...this.workers].map((Q)=>new Promise((Z,$)=>{let V=this.nextId++;this.broadcasts.set(V,{worker:Q,resolve:Z,reject:$});try{Q.postMessage({__id:V,request:G},J)}catch(z){this.broadcasts.delete(V),$(z)}})))}dispose(){this.disposed=!0;for(let J of this.workers)J.terminate();let G=Error("WorkerPool disposed");for(let{job:J}of this.inflight.values())this.rejectJob(J,G);for(let J of this.queue)this.rejectJob(J,G);for(let J of this.broadcasts.values())J.reject(G);this.broadcasts.clear(),this.inflight.clear(),this.byWorker.clear(),this.queue.length=0,this.idle.length=0,this.workers.length=0}resolveJob(G,J){if(G.settled)return;G.settled=!0,this.detachAbort(G),G.resolve(J)}rejectJob(G,J){if(G.settled)return;G.settled=!0,this.detachAbort(G),G.reject(J)}detachAbort(G){if(G.signal&&G.onAbort)G.signal.removeEventListener("abort",G.onAbort)}pump(){while(this.idle.length>0&&this.queue.length>0){let G=this.idle.pop(),J=this.queue.shift(),Q=this.nextId++;this.inflight.set(Q,{job:J,worker:G}),this.byWorker.set(G,Q);try{G.postMessage({__id:Q,request:J.request},J.transfer)}catch(Z){this.inflight.delete(Q),this.byWorker.delete(G),this.idle.push(G),this.rejectJob(J,Z)}}}onMessage(G,J){let{__id:Q,result:Z,error:$}=J.data,V=this.broadcasts.get(Q);if(V){if(this.broadcasts.delete(Q),$!==void 0)V.reject(Error($));else V.resolve(Z);return}let z=this.inflight.get(Q);if(!z)return;if(this.inflight.delete(Q),this.byWorker.delete(G),this.idle.push(G),z.job.settled){if($===void 0&&this.onDiscard)this.onDiscard(Z)}else if($!==void 0)this.rejectJob(z.job,Error($));else this.resolveJob(z.job,Z);this.pump()}onError(G,J){if(!this.workers.includes(G))return;let Q=this.byWorker.get(G),Z=Q!==void 0?this.inflight.get(Q):void 0;if(Q!==void 0)this.inflight.delete(Q);let $=Error(J.message||"worker error");for(let[V,z]of this.broadcasts)if(z.worker===G)this.broadcasts.delete(V),z.reject($);if(this.replaceWorker(G),Z)this.rejectJob(Z.job,$);this.pump()}replaceWorker(G){let J=Error("worker replaced before broadcast reply");for(let[$,V]of this.broadcasts)if(V.worker===G)this.broadcasts.delete($),V.reject(J);this.byWorker.delete(G);let Q=this.idle.indexOf(G);if(Q>=0)this.idle.splice(Q,1);let Z=this.workers.indexOf(G);if(Z>=0)this.workers.splice(Z,1);try{G.terminate()}catch{}if(this.disposed||this.broken)return;if(++this.respawns>this.targetSize*3+2){this.fail(Error("WorkerPool: workers repeatedly faulted (check the worker module + wasm URL)"));return}try{this.addWorker()}catch{if(this.workers.length===0)this.fail(Error("WorkerPool: failed to spawn a replacement worker"))}}fail(G){this.broken=G;for(let J of this.queue)this.rejectJob(J,G);this.queue.length=0}}function l(G){let J=null,Q=null;return{set(Z){J=Z},has(){return J!==null},wasmUrl(){return J?.wasmUrl},source(){if(!J)return Promise.reject(Error(`${G}: no CDN runtime registered`));let{workerUrl:Z}=J;return Q??=fetch(Z).then(($)=>{if(!$.ok)throw Q=null,Error(`${G}: worker chunk fetch ${$.status} for ${Z}`);return $.text()}),Q},spawnFromSource(Z){let $=URL.createObjectURL(new Blob([Z],{type:"text/javascript"}));try{return new Worker($)}catch(V){throw Error(`${G}: blob worker construction failed — page CSP likely lacks \`worker-src blob:\`; pass \`spawn\` with a self-hosted worker script`,{cause:V})}finally{URL.revokeObjectURL($)}}}}var B=["BVX_WASM_LOAD_FAILED","BVX_DECODE_FAILED","BVX_NO_BACKEND","BVX_WORKER_SPAWN_BLOCKED","BVX_GL_UNAVAILABLE","BVX_BAD_INPUT","BVX_ASSET_FETCH_FAILED"];class H extends Error{name="BitruviusError";code;constructor(G,J,Q){super(`${G}: ${J}`,Q);this.code=G}static is(G,J){let Q=G;return G instanceof Error&&typeof Q?.code==="string"&&B.includes(Q.code)&&(J===void 0||Q.code===J)}}function n(G,J,Q){if(H.is(Q))return Q;return new H(G,J,{cause:Q})}var F=new Set;function s(){F.clear()}function t(G){let{label:J,cause:Q,fallback:Z,chunk:$,disableOption:V,onDegraded:z}=G,X=Q instanceof Error?Q.message:String(Q),Y=["serve the SDK same-origin","allow `worker-src blob:` in your CSP",...$?[`self-host \`${$}\` and pass \`spawn\``]:[],...V?[`or pass \`${V}\` to choose this path deliberately`]:[]].join(", "),K=new H("BVX_WORKER_SPAWN_BLOCKED",`${J}: worker pool unavailable — falling back to ${Z}, which is MUCH slower and runs on the render thread. ${X}. Fixes: ${Y}.`,{cause:Q});if(!F.has(J))F.add(J),console.warn(K.message);return z?.(K),K}function O(G){if(!G)return{};if(G instanceof Headers){let J={};return G.forEach((Q,Z)=>{J[Z]=Q}),J}if(Array.isArray(G))return Object.fromEntries(G);return{...G}}function P(G,J){if(!G)return J??{};if(!J)return G;let Q={...G,...J};if(G.headers||J.headers)Q.headers={...O(G.headers),...O(J.headers)};return Q}var T=new Set(["authorization","proxy-authorization","cookie","x-goog-api-key","x-api-key","x-esri-authorization"]),y=["cesium.com","googleapis.com"],I="https://relative.invalid/",E="relative.invalid";function C(G){let J;try{J=new URL(G,I)}catch{return}if(J.hostname===E)return;return{host:J.hostname.toLowerCase(),protocol:J.protocol}}function f(G,J){return J.some((Q)=>G===Q||G.endsWith(`.${Q}`))}function j(G){if(!G)return G;let J=O(G.headers),Q=Object.keys(J).filter((V)=>T.has(V.toLowerCase())),Z=G.credentials==="include";if(Q.length===0&&!Z)return G;let $={...G};if(Q.length>0){for(let V of Q)delete J[V];$.headers=J}if(Z)$.credentials="same-origin";return $}function N(G,J={}){let Q=j(G);if(Q===G)return{fetch:(z,X)=>globalThis.fetch(z,P(G,X))};let Z;if(J.credentialOrigin!==void 0){if(Z=C(J.credentialOrigin),Z===void 0||Z.host==="")throw Error(`publicAuthorizer: credentialOrigin must be an absolute URL naming the origin the credential was minted for (e.g. "https://tiles.example.com/tileset.json"); received ${JSON.stringify(J.credentialOrigin)}.`)}let $=[...y,...J.trustedHosts??[]].map((z)=>z.toLowerCase()),V=new Set;return{fetch(z,X){let Y=C(z);if(Y===void 0)return globalThis.fetch(z,P(G,X));let{host:K,protocol:M}=Y;if(Z===void 0&&K!=="")Z=Y;let R=M==="https:"||M===Z?.protocol,x=K!==""&&(K===Z?.host||f(K,$));if(x&&R)return globalThis.fetch(z,P(G,X));if(!V.has(K)){V.add(K);let S=x?`${M}//${K} is a cleartext url, so the request was sent without this session's credential. A header sent over http is readable by anything on the path; serve it over https.`:K===""?`a ${M} url names no host, so the request was sent without this session's credential.`:`${K} is not the origin this session's credential was issued for (${Z?.host??"unknown"}), so the request was sent without it. If you own that host, pass it on the layer: auth: publicAuthorizer(requestInit, { trustedHosts: ['${K}'] }).`;console.warn(`[bitruvius] ${S}`)}return globalThis.fetch(z,P(Q,X))}}}var v=/^\s*(?:text\/|application\/(?:json|xml|xhtml\+xml)\b)/i,g=[[60,33],[60,104],[60,63]];async function GG(G,J={}){let{label:Q,...Z}=J,$=Q?`${Q}: `:"",V=await fetch(G,Z);if(!V.ok)throw new H("BVX_ASSET_FETCH_FAILED",`${$}${G} returned HTTP ${V.status}${V.statusText?` ${V.statusText}`:""} — expected a binary asset.`);let z=V.headers.get("content-type")??"";if(v.test(z))throw new H("BVX_ASSET_FETCH_FAILED",`${$}${G} served "${z.split(";")[0].trim()}", not a binary asset — the file is probably `+"missing and the server returned a fallback page. Check the URL and that the asset is deployed.");let X=new Uint8Array(await V.arrayBuffer());if(!z&&g.some(([Y,K])=>X[0]===Y&&X[1]===K))throw new H("BVX_ASSET_FETCH_FAILED",`${$}${G} returned a markup document, not a binary asset — the file is probably missing and the `+"server returned a fallback page. Check the URL and that the asset is deployed.");return X}var q="__BITRUVIUS_CONFIG__";function ZG(G){let J=globalThis;J[q]={...J[q],...G}}function D(){return globalThis[q]??{}}function $G(G,J,Q){if(G)return G;let Z=D().esriAuth;if(Z)return Z;return N(J,Q)}function VG(G){let J=D().wasmBaseUrl;if(J===void 0)return;return new URL(G,J instanceof URL?J:new URL(J,globalThis.location?.href))}export{$G as resolveAuth,t as reportWorkerDegradation,m as recommendDecodeBackend,N as publicAuthorizer,P as mergeInit,L as isWasmSimdSupported,p as isAbortError,D as getConfig,GG as fetchBinaryAsset,k as detectCapabilities,l as createCdnWorkerTrampoline,VG as configuredWasmUrl,ZG as configure,n as asBitruviusError,W as abortError,s as __resetWorkerDegradationLog,_ as WorkerPool,U as TaskQueue,y as DEFAULT_TRUSTED_CREDENTIAL_HOSTS,H as BitruviusError,B as BVX_CODES};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WHATWG abort convention without `DOMException` (not constructible in
|
|
3
|
+
* every worker/test realm): an `Error` whose `name` is `'AbortError'`.
|
|
4
|
+
* Consumers that dispatch on cancellation (MapLibre's `isAbortError`, our
|
|
5
|
+
* loaders) check exactly that name.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Build an abort rejection.
|
|
9
|
+
*
|
|
10
|
+
* @param reason - Optional `AbortSignal.reason` to carry in the message.
|
|
11
|
+
* @returns An `Error` named `'AbortError'`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function abortError(reason?: unknown): Error;
|
|
14
|
+
/**
|
|
15
|
+
* Whether a rejection is a cancellation (ours or a native fetch abort).
|
|
16
|
+
*
|
|
17
|
+
* @param err - The caught value.
|
|
18
|
+
* @returns `true` for any `Error`/`DOMException` named `'AbortError'`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isAbortError(err: unknown): boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cross-origin Web Worker **blob trampoline** — one mechanism, instantiated once per
|
|
3
|
+
* published worker chunk.
|
|
4
|
+
*
|
|
5
|
+
* `new Worker(crossOriginUrl, { type: 'module' })` throws a SYNCHRONOUS `SecurityError`
|
|
6
|
+
* in the constructor: worker scripts must be same-origin (or `blob:`/`data:`). The check
|
|
7
|
+
* runs BEFORE any network request, so publishing the missing chunk cannot fix it — a page
|
|
8
|
+
* that loads the SDK from a CDN simply cannot spawn the SDK's worker chunks directly.
|
|
9
|
+
*
|
|
10
|
+
* The fix: fetch the published, self-contained worker chunk's TEXT (a plain CORS GET the
|
|
11
|
+
* CDN serves happily) and spawn it from a same-origin `blob:` URL, which inherits the
|
|
12
|
+
* page's origin. Blob workers can't resolve relative imports (`blob:https://host/uuid` is
|
|
13
|
+
* not hierarchical, so every specifier fails), so the published chunk must be a fully
|
|
14
|
+
* self-contained CLASSIC script — see the `*-decode-worker.iife.min.js` steps in
|
|
15
|
+
* `scripts/build-cdn.ts`. Codec wasm is resolved on the MAIN thread and forwarded by URL
|
|
16
|
+
* or bytes, never resolved by the worker against its blob base.
|
|
17
|
+
*
|
|
18
|
+
* `set()` is called by the generated CDN bundle entry at module init. npm/bundler
|
|
19
|
+
* consumers never register anything, so `has()` stays false and their decoder keeps
|
|
20
|
+
* spawning the same-origin module worker their bundler emitted — same-origin behaviour is
|
|
21
|
+
* unchanged by construction.
|
|
22
|
+
*
|
|
23
|
+
* CSP note: blob workers need `worker-src blob:`. Locked-down consumers host the published
|
|
24
|
+
* chunk themselves and pass `spawn: () => new Worker('/their/path.js')`.
|
|
25
|
+
*/
|
|
26
|
+
/** What the CDN bundle entry registers for one published worker chunk. */
|
|
27
|
+
export interface CdnWorkerRuntime {
|
|
28
|
+
/** Absolute URL of this build's published, self-contained classic worker chunk. */
|
|
29
|
+
readonly workerUrl: string;
|
|
30
|
+
/**
|
|
31
|
+
* Absolute URL of the codec wasm this worker needs, when the worker's own package-relative
|
|
32
|
+
* resolution can't survive a blob base (wasm-bindgen glue resolves its `_bg.wasm` against
|
|
33
|
+
* `import.meta.url`). Resolved at BUILD time against the published tree, so the decoder can
|
|
34
|
+
* forward it on the main thread. Omit for workers handed their wasm URL another way.
|
|
35
|
+
*/
|
|
36
|
+
readonly wasmUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
/** One published worker chunk's trampoline. Created once per chunk at module scope. */
|
|
39
|
+
export interface CdnWorkerTrampoline {
|
|
40
|
+
/** @internal Register this build's chunk — called by the generated CDN bundle entry. */
|
|
41
|
+
set(rt: CdnWorkerRuntime): void;
|
|
42
|
+
/** True once a CDN bundle registered a chunk (i.e. take the blob path, not `new Worker(url)`). */
|
|
43
|
+
has(): boolean;
|
|
44
|
+
/** The registered wasm URL, if the build published one. */
|
|
45
|
+
wasmUrl(): string | undefined;
|
|
46
|
+
/** Fetch (once, cached across the whole pool) the chunk's source text. */
|
|
47
|
+
source(): Promise<string>;
|
|
48
|
+
/** Spawn a classic worker from already-fetched source via a same-origin Blob URL. */
|
|
49
|
+
spawnFromSource(source: string): Worker;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create the trampoline for one published worker chunk.
|
|
53
|
+
*
|
|
54
|
+
* @param label - Package/worker name used in error messages (e.g. `'tiles3d-mesh'`).
|
|
55
|
+
* @returns The per-chunk trampoline; keep it at module scope so the source fetch is
|
|
56
|
+
* shared by every worker in the pool.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createCdnWorkerTrampoline(label: string): CdnWorkerTrampoline;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal concurrency-limited async task runner. Used to bound how many
|
|
3
|
+
* decode/work jobs run at once (e.g. the worker-pool fan-out), without
|
|
4
|
+
* pulling in a dependency. Pure — no DOM/worker deps.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare class TaskQueue {
|
|
9
|
+
private readonly concurrency;
|
|
10
|
+
private active;
|
|
11
|
+
private readonly pending;
|
|
12
|
+
private readonly idleWaiters;
|
|
13
|
+
constructor(concurrency: number);
|
|
14
|
+
/** Total tasks not yet settled (queued + running). */
|
|
15
|
+
get size(): number;
|
|
16
|
+
/** Queue `fn`; resolves/rejects with its result once it has run. */
|
|
17
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
18
|
+
/** Resolves when there are no queued or running tasks. */
|
|
19
|
+
onIdle(): Promise<void>;
|
|
20
|
+
private afterTask;
|
|
21
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time, LOUD reporting of a worker-pool degradation.
|
|
3
|
+
*
|
|
4
|
+
* When a decode worker pool cannot be spawned, every decoder in the SDK can still decode
|
|
5
|
+
* on the calling thread — so the picture the consumer sees is a layer that renders, just
|
|
6
|
+
* far slower, or (worse) a layer that silently renders nothing while `console.warn` scrolls
|
|
7
|
+
* eight lines past. Both failure modes were shipping: a cross-origin CDN page got an empty
|
|
8
|
+
* mesh layer with no catchable error, and Esri terrain rendered a perfectly flat planet
|
|
9
|
+
* with no output at all.
|
|
10
|
+
*
|
|
11
|
+
* This helper is the single place that decides what a degradation says. It:
|
|
12
|
+
* - builds a `BitruviusError('BVX_WORKER_SPAWN_BLOCKED')` the caller memoises, so the
|
|
13
|
+
* pool is attempted ONCE rather than re-thrown per tile (the spawn storm);
|
|
14
|
+
* - warns exactly once per label, naming the real cost and every escape hatch;
|
|
15
|
+
* - hands the error to the layer's `onError` so a consumer can react in code.
|
|
16
|
+
*/
|
|
17
|
+
import { BitruviusError } from '../errors';
|
|
18
|
+
/** How one worker path degrades; see {@link reportWorkerDegradation}. */
|
|
19
|
+
export interface WorkerDegradationReport {
|
|
20
|
+
/** Package/worker name, used as the log prefix and the once-per-label key. */
|
|
21
|
+
readonly label: string;
|
|
22
|
+
/** The underlying failure (usually a `SecurityError` from the `Worker` constructor). */
|
|
23
|
+
readonly cause: unknown;
|
|
24
|
+
/** What the SDK does instead — e.g. `'main-thread decode'`. */
|
|
25
|
+
readonly fallback: string;
|
|
26
|
+
/** Published chunk filename for the self-host escape hatch, when there is one. */
|
|
27
|
+
readonly chunk?: string;
|
|
28
|
+
/** Layer/decoder option that selects the fallback deliberately, e.g. `'workers: false'`. */
|
|
29
|
+
readonly disableOption?: string;
|
|
30
|
+
/** Layer `onError` (or any consumer sink) — always called, even after the log is deduped. */
|
|
31
|
+
readonly onDegraded?: (err: BitruviusError) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reset the once-per-label log dedupe.
|
|
35
|
+
*
|
|
36
|
+
* @remarks Test seam only — production code never un-warns.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export declare function __resetWorkerDegradationLog(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Build (and, once per label, log) the error describing a worker-pool degradation.
|
|
42
|
+
*
|
|
43
|
+
* @param report - What degraded and how; see {@link WorkerDegradationReport}.
|
|
44
|
+
* @returns The `BitruviusError` to memoise on the decoder — its presence is what stops
|
|
45
|
+
* the caller re-attempting the spawn on every subsequent decode.
|
|
46
|
+
*/
|
|
47
|
+
export declare function reportWorkerDegradation(report: WorkerDegradationReport): BitruviusError;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Options for {@link WorkerPool}. @internal */
|
|
2
|
+
export interface WorkerPoolOptions<TReq = unknown> {
|
|
3
|
+
/** Number of workers to spawn. */
|
|
4
|
+
size: number;
|
|
5
|
+
/** Create a fresh module worker. */
|
|
6
|
+
spawn: () => Worker;
|
|
7
|
+
/**
|
|
8
|
+
* Optional init request posted to EACH worker (including self-heal
|
|
9
|
+
* replacements) before it accepts jobs — e.g. wasm instantiation, so the
|
|
10
|
+
* first real task doesn't pay the compile. The worker must ack through the
|
|
11
|
+
* normal reply protocol; an error reply pulls the worker via the budgeted
|
|
12
|
+
* respawn path (a broken wasm URL fails the pool loudly, not silently).
|
|
13
|
+
* Called once per worker, so per-worker payloads (cloned bytes) are safe.
|
|
14
|
+
*/
|
|
15
|
+
warmup?: () => {
|
|
16
|
+
request: TReq;
|
|
17
|
+
transfer?: Transferable[];
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Called with a result whose task was abandoned (aborted in flight, so the
|
|
21
|
+
* worker's late reply has no consumer) — release resources the reply carries
|
|
22
|
+
* (e.g. `ImageBitmap.close()`) instead of waiting on GC.
|
|
23
|
+
*/
|
|
24
|
+
onDiscard?: (result: unknown) => void;
|
|
25
|
+
}
|
|
26
|
+
/** Per-call options for {@link WorkerPool.run}. @internal */
|
|
27
|
+
export interface WorkerPoolRunOptions {
|
|
28
|
+
/** Abort the task: queued → dropped before dispatch; in-flight → caller
|
|
29
|
+
* rejected now, late worker reply discarded. Rejections carry
|
|
30
|
+
* `name === 'AbortError'`. */
|
|
31
|
+
readonly signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
/** @internal */
|
|
34
|
+
export declare class WorkerPool<TReq = unknown, TRes = unknown> {
|
|
35
|
+
private readonly workers;
|
|
36
|
+
private readonly idle;
|
|
37
|
+
private readonly queue;
|
|
38
|
+
private readonly inflight;
|
|
39
|
+
private readonly byWorker;
|
|
40
|
+
private readonly broadcasts;
|
|
41
|
+
private nextId;
|
|
42
|
+
private disposed;
|
|
43
|
+
private readonly spawn;
|
|
44
|
+
private readonly targetSize;
|
|
45
|
+
private respawns;
|
|
46
|
+
private broken;
|
|
47
|
+
private readonly warmup?;
|
|
48
|
+
private readonly onDiscard?;
|
|
49
|
+
private warmPending;
|
|
50
|
+
private readonly warmWaiters;
|
|
51
|
+
constructor(opts: WorkerPoolOptions<TReq>);
|
|
52
|
+
private addWorker;
|
|
53
|
+
private noteWarm;
|
|
54
|
+
/**
|
|
55
|
+
* Resolves once every currently-spawned worker has acked its warm-up (or the
|
|
56
|
+
* pool gave up on it). Immediately resolved when no `warmup` option is set.
|
|
57
|
+
*/
|
|
58
|
+
whenWarm(): Promise<void>;
|
|
59
|
+
/** Queued + in-flight task count. */
|
|
60
|
+
get size(): number;
|
|
61
|
+
/** Tasks waiting for a worker (not yet dispatched). */
|
|
62
|
+
get queueLength(): number;
|
|
63
|
+
/** Tasks currently running on a worker (abandoned-but-running included). */
|
|
64
|
+
get inflightCount(): number;
|
|
65
|
+
/**
|
|
66
|
+
* Run one task; resolves with the worker's `result` (or rejects on error /
|
|
67
|
+
* abort — see {@link WorkerPoolRunOptions}).
|
|
68
|
+
*/
|
|
69
|
+
run(request: TReq, transfer?: Transferable[], opts?: WorkerPoolRunOptions): Promise<TRes>;
|
|
70
|
+
/**
|
|
71
|
+
* Send one request to EVERY worker (busy or idle) and resolve with one
|
|
72
|
+
* result per worker (rejects if any worker errors or faults). Replies are
|
|
73
|
+
* correlated by the same `__id` wire mechanism as {@link run}, but
|
|
74
|
+
* broadcast replies never touch the idle/dispatch state — a broadcast
|
|
75
|
+
* coexists with the normal queue (workers drain messages in order).
|
|
76
|
+
*
|
|
77
|
+
* NOTE on `transfer`: a Transferable is detached by the FIRST post, so a
|
|
78
|
+
* transfer list is only safe with a single-worker pool. SharedArrayBuffers
|
|
79
|
+
* don't need (and can't be in) a transfer list — they share zero-copy.
|
|
80
|
+
*/
|
|
81
|
+
broadcast(request: TReq, transfer?: Transferable[]): Promise<TRes[]>;
|
|
82
|
+
/** Terminate all workers and reject any outstanding work. */
|
|
83
|
+
dispose(): void;
|
|
84
|
+
/** Settle helpers: at-most-once + abort-listener hygiene. */
|
|
85
|
+
private resolveJob;
|
|
86
|
+
private rejectJob;
|
|
87
|
+
private detachAbort;
|
|
88
|
+
private pump;
|
|
89
|
+
private onMessage;
|
|
90
|
+
private onError;
|
|
91
|
+
/** Remove a faulted worker from rotation, terminate it, and (within a respawn
|
|
92
|
+
* budget) spawn a fresh replacement. If workers keep faulting — a broken worker
|
|
93
|
+
* module/wasm URL — fail the pool loudly instead of spinning or hanging. */
|
|
94
|
+
private replaceWorker;
|
|
95
|
+
/** Mark the pool broken and reject all queued (and future) work. */
|
|
96
|
+
private fail;
|
|
97
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global SDK configuration — one call wires wasm asset resolution (and a shared
|
|
3
|
+
* ESRI sign-in) for every codec, instead of passing wasm options per decoder.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* The configuration store lives on `globalThis` under a namespaced key, so
|
|
7
|
+
* independently loaded copies of the SDK (e.g. two per-package CDN bundles on one
|
|
8
|
+
* page) share ONE configuration.
|
|
9
|
+
*
|
|
10
|
+
* **wasm asset precedence (applies to every codec's `ensureWasm`, documented once
|
|
11
|
+
* here).** When a decoder needs its `.wasm`, it resolves the bytes/URL in this order,
|
|
12
|
+
* highest first:
|
|
13
|
+
* 1. an explicit `wasmBytes` (raw `Uint8Array`) or `wasmUrl` passed to that decoder;
|
|
14
|
+
* 2. {@link configure}`({ wasmBaseUrl })` joined with the codec's wasm filename
|
|
15
|
+
* (see `configuredWasmUrl`);
|
|
16
|
+
* 3. the build's default (a bundler-resolved sibling file on npm; a baked,
|
|
17
|
+
* content-hashed URL in CDN bundles).
|
|
18
|
+
*
|
|
19
|
+
* @see {@link configure} to set it, {@link getConfig} to read it,
|
|
20
|
+
* `configuredWasmUrl` for the per-codec wasm URL resolution.
|
|
21
|
+
*/
|
|
22
|
+
import { type PublicAuthorizerOptions, type RequestAuthorizer } from './transport/authorizer';
|
|
23
|
+
/**
|
|
24
|
+
* Global configuration shape merged by {@link configure} and read by
|
|
25
|
+
* {@link getConfig}. Every field is optional; an unset field falls through to the
|
|
26
|
+
* documented per-decoder / build default.
|
|
27
|
+
*/
|
|
28
|
+
export interface BitruviusConfig {
|
|
29
|
+
/**
|
|
30
|
+
* Base URL the codec `.wasm` files are served from. Resolved per codec as
|
|
31
|
+
* `new URL(<wasmFileName>, wasmBaseUrl)` (see `configuredWasmUrl`) — include a
|
|
32
|
+
* trailing slash. This is step 2 of the wasm precedence documented on
|
|
33
|
+
* {@link BitruviusConfig}; an explicit per-decoder `wasmUrl`/`wasmBytes` overrides it.
|
|
34
|
+
*/
|
|
35
|
+
readonly wasmBaseUrl?: string | URL;
|
|
36
|
+
/**
|
|
37
|
+
* Default request authorizer applied to every ESRI/ArcGIS layer fetch that does
|
|
38
|
+
* not set its own `auth`. Wired by `configureEsriAuth()` in `@bitruvius/esri-auth`,
|
|
39
|
+
* which calls `configure({ esriAuth })` here so one sign-in is shared across all
|
|
40
|
+
* layers (and across independently loaded per-package CDN bundles). Layers consume
|
|
41
|
+
* it via {@link resolveAuth}; absent ⇒ public-first unauthenticated fetches.
|
|
42
|
+
*/
|
|
43
|
+
readonly esriAuth?: RequestAuthorizer;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Set (merge) the global SDK configuration. This is the first call most
|
|
47
|
+
* applications make: it wires wasm asset resolution (and, via `@bitruvius/esri-auth`,
|
|
48
|
+
* a shared sign-in) for every codec/layer at once.
|
|
49
|
+
*
|
|
50
|
+
* @remarks
|
|
51
|
+
* Each call MERGES field-wise over the previous configuration — later calls override
|
|
52
|
+
* only the fields they set. The store is shared across independently loaded SDK
|
|
53
|
+
* bundles on one page (see {@link BitruviusConfig}).
|
|
54
|
+
*
|
|
55
|
+
* @param config - The fields to set/override. See {@link BitruviusConfig}.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { configure } from '@bitruvius/foundation';
|
|
60
|
+
*
|
|
61
|
+
* // Serve every codec's .wasm from one CDN folder (note the trailing slash).
|
|
62
|
+
* configure({ wasmBaseUrl: 'https://my.app/assets/bitruvius/' });
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @see {@link getConfig}, `configuredWasmUrl`, {@link BitruviusConfig.wasmBaseUrl}
|
|
66
|
+
*/
|
|
67
|
+
export declare function configure(config: BitruviusConfig): void;
|
|
68
|
+
/**
|
|
69
|
+
* Read the current global SDK configuration set by {@link configure}.
|
|
70
|
+
*
|
|
71
|
+
* @returns The merged {@link BitruviusConfig}; an empty object when {@link configure}
|
|
72
|
+
* has never been called.
|
|
73
|
+
*/
|
|
74
|
+
export declare function getConfig(): BitruviusConfig;
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the {@link RequestAuthorizer} a layer fetch should use. Always returns a
|
|
77
|
+
* usable authorizer, so every call site has ONE code path.
|
|
78
|
+
*
|
|
79
|
+
* @remarks
|
|
80
|
+
* Precedence, highest first:
|
|
81
|
+
* 1. the layer's own `auth` option (`perLayer`);
|
|
82
|
+
* 2. the global {@link configure}`({ esriAuth })` session;
|
|
83
|
+
* 3. a no-op {@link publicAuthorizer} wrapping the layer's static `staticInit`.
|
|
84
|
+
*
|
|
85
|
+
* `auth` (per-layer or global) and `requestInit` are alternatives: when a real
|
|
86
|
+
* authorizer is present it owns header attachment, so `staticInit` is applied ONLY by
|
|
87
|
+
* the public fallback.
|
|
88
|
+
*
|
|
89
|
+
* @param perLayer - The layer's own authorizer, if any.
|
|
90
|
+
* @param staticInit - Static `RequestInit` (headers etc.) the layer supplied; used
|
|
91
|
+
* only by the public-fallback path.
|
|
92
|
+
* @returns A {@link RequestAuthorizer} that is always safe to call.
|
|
93
|
+
* @see {@link BitruviusConfig.esriAuth}, {@link publicAuthorizer}
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveAuth(perLayer?: RequestAuthorizer, staticInit?: RequestInit, options?: PublicAuthorizerOptions): RequestAuthorizer;
|
|
96
|
+
/**
|
|
97
|
+
* Resolve a codec's wasm filename against the {@link configure}`({ wasmBaseUrl })`
|
|
98
|
+
* base URL — step 2 of the wasm precedence documented on {@link BitruviusConfig}.
|
|
99
|
+
* Used by every codec's `ensureWasm`.
|
|
100
|
+
*
|
|
101
|
+
* @remarks
|
|
102
|
+
* Resolved as `new URL(wasmFileName, wasmBaseUrl)`, so `wasmBaseUrl` should end with a
|
|
103
|
+
* trailing slash. A string base is itself resolved against `location.href` first (so a
|
|
104
|
+
* relative base works in the browser).
|
|
105
|
+
*
|
|
106
|
+
* @param wasmFileName - The codec's bare wasm filename (e.g. `'bvc_wasm_bg.wasm'`).
|
|
107
|
+
* @returns The absolute wasm {@link URL}, or `undefined` when no `wasmBaseUrl` is
|
|
108
|
+
* configured (the caller then falls through to the build default).
|
|
109
|
+
* @see {@link configure}, {@link BitruviusConfig.wasmBaseUrl}
|
|
110
|
+
*/
|
|
111
|
+
export declare function configuredWasmUrl(wasmFileName: string): URL | undefined;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport authorization seam — a drop-in replacement for the global `fetch`,
|
|
3
|
+
* scoped to one streaming session.
|
|
4
|
+
*
|
|
5
|
+
* Every streaming engine (3D-Tiles, I3S, elevation) routes its network requests
|
|
6
|
+
* through a {@link RequestAuthorizer.fetch}. The default {@link publicAuthorizer}
|
|
7
|
+
* adds nothing and never retries, so a public URL does EXACTLY ONE unauthenticated
|
|
8
|
+
* request — byte-identical to the pre-auth behavior, no perf regression. A real
|
|
9
|
+
* implementation (e.g. the ESRI identity manager in `@bitruvius/esri-auth`)
|
|
10
|
+
* attaches per-host auth (a header or a `?token=` query param) and transparently
|
|
11
|
+
* refreshes + retries ONCE on an auth challenge.
|
|
12
|
+
*
|
|
13
|
+
* Why a `fetch`-override and not an `authorize()`/`onChallenge()` pair:
|
|
14
|
+
* - every call site already calls `fetch`, so this is a literal drop-in; and
|
|
15
|
+
* - only the override holds the `Response`, so it can detect ArcGIS's `f=json`
|
|
16
|
+
* challenge — HTTP **200** with a body of `{ error: { code: 498 | 499 } }` —
|
|
17
|
+
* which binary endpoints instead signal as HTTP 401/403/498/499.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* A drop-in replacement for the global `fetch`, scoped to one streaming session. Pass a
|
|
21
|
+
* custom implementation as a layer's `auth` option (or globally via
|
|
22
|
+
* {@link configure}`({ esriAuth })`) to attach per-host credentials and transparently
|
|
23
|
+
* refresh + retry on an auth challenge; the default is {@link publicAuthorizer}.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const auth: RequestAuthorizer = {
|
|
28
|
+
* async fetch(url, init) {
|
|
29
|
+
* const token = await getToken();
|
|
30
|
+
* return fetch(url, mergeInit(init, { headers: { Authorization: `Bearer ${token}` } }));
|
|
31
|
+
* },
|
|
32
|
+
* };
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @see {@link publicAuthorizer}, {@link resolveAuth}, {@link BitruviusConfig.esriAuth}
|
|
36
|
+
*/
|
|
37
|
+
export interface RequestAuthorizer {
|
|
38
|
+
/**
|
|
39
|
+
* Issue an authorized request and resolve the FINAL {@link Response} (after any
|
|
40
|
+
* internal token acquisition / refresh + single retry).
|
|
41
|
+
* @param input - The request URL.
|
|
42
|
+
* @param init - Standard `RequestInit`. If `init.signal` is present (the streaming
|
|
43
|
+
* engines pass the tile's abort + a hard timeout) it MUST be honored.
|
|
44
|
+
* @returns The final `Response`.
|
|
45
|
+
*/
|
|
46
|
+
fetch(input: string, init?: RequestInit): Promise<Response>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Merge two `RequestInit`s into one — useful when writing a {@link RequestAuthorizer}
|
|
50
|
+
* that layers credentials over the caller's init.
|
|
51
|
+
* @param a - The base init (lower precedence).
|
|
52
|
+
* @param b - The override init (wins on top-level conflicts; its headers win field-wise).
|
|
53
|
+
* @returns A new merged `RequestInit` (`headers` combined field-wise). Either side may
|
|
54
|
+
* be `undefined`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function mergeInit(a?: RequestInit, b?: RequestInit): RequestInit;
|
|
57
|
+
/**
|
|
58
|
+
* Hosts that receive a session credential without the caller naming them: the service
|
|
59
|
+
* domains whose credentials this SDK mints (Cesium ion, Google Map Tiles). Anything
|
|
60
|
+
* else — a customer's own tile server, a sibling CDN — is trusted only because it IS
|
|
61
|
+
* the origin the credential was minted for, or because the caller listed it in
|
|
62
|
+
* {@link PublicAuthorizerOptions.trustedHosts}.
|
|
63
|
+
*/
|
|
64
|
+
export declare const DEFAULT_TRUSTED_CREDENTIAL_HOSTS: readonly string[];
|
|
65
|
+
/** Options for {@link publicAuthorizer}'s credential trust boundary. */
|
|
66
|
+
export interface PublicAuthorizerOptions {
|
|
67
|
+
/**
|
|
68
|
+
* Absolute URL (or origin) the credential in `staticInit` was minted for — normally
|
|
69
|
+
* the tileset document's own URL. Requests to any other host are sent WITHOUT the
|
|
70
|
+
* credential. Omit and the credential pins to the host of the first request instead;
|
|
71
|
+
* naming it is strictly better, because the first request's host comes from a url the
|
|
72
|
+
* third-party tileset chose, so the fallback anchor is attacker-influenced.
|
|
73
|
+
*
|
|
74
|
+
* Must name a host: `file:///…` and `data:` URLs are rejected rather than accepted as
|
|
75
|
+
* an anchor that matches nothing.
|
|
76
|
+
*/
|
|
77
|
+
readonly credentialOrigin?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Extra hosts allowed to receive the credential — the legitimate case of content URLs
|
|
80
|
+
* on a sibling CDN host of the same service. Matched on a dot boundary, so
|
|
81
|
+
* `acme.com` covers `cdn.acme.com` but never `notacme.com`.
|
|
82
|
+
*/
|
|
83
|
+
readonly trustedHosts?: readonly string[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The default no-op / public {@link RequestAuthorizer}: merges `staticInit` UNDER each
|
|
87
|
+
* call's init and delegates to the global `fetch`. A public URL does EXACTLY ONE
|
|
88
|
+
* unauthenticated request with no retry — byte-identical to plain `fetch`.
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
91
|
+
* When `staticInit` carries a credential (see {@link CREDENTIAL_HEADERS} —
|
|
92
|
+
* a Cesium ion `Bearer`, a Google `X-Goog-Api-Key`, cookies) that credential is sent
|
|
93
|
+
* ONLY to the origin it was minted for, plus {@link DEFAULT_TRUSTED_CREDENTIAL_HOSTS}
|
|
94
|
+
* and any {@link PublicAuthorizerOptions.trustedHosts}. Every other host gets the
|
|
95
|
+
* request without it, and is warned about once.
|
|
96
|
+
*
|
|
97
|
+
* A tileset is a document fetched from a third party that lists further URLs, and the
|
|
98
|
+
* streaming engines fetch every URL it names through this authorizer. Without the
|
|
99
|
+
* boundary, a tileset that names `https://attacker.example/tile.glb` harvests the ion
|
|
100
|
+
* token or the API key — both reusable, both billable to the user — delivered by our own
|
|
101
|
+
* transport, with the response body proving to the attacker that the credential works.
|
|
102
|
+
* A credential minted for host A does not become valid for host B because A's JSON said
|
|
103
|
+
* so, so a cross-origin URL is fetched public instead of authenticated: the tile fails
|
|
104
|
+
* to load, which is recoverable, rather than the key leaking, which is not.
|
|
105
|
+
*
|
|
106
|
+
* @param staticInit - Optional static `RequestInit` (e.g. headers) applied beneath each
|
|
107
|
+
* request's own init.
|
|
108
|
+
* @param options - Credential trust boundary; see {@link PublicAuthorizerOptions}. Ignored
|
|
109
|
+
* when `staticInit` carries no credential.
|
|
110
|
+
* @returns A {@link RequestAuthorizer} backed by the global `fetch`.
|
|
111
|
+
* @throws Error when `options.credentialOrigin` is not a parseable absolute URL.
|
|
112
|
+
* @see {@link resolveAuth}
|
|
113
|
+
*/
|
|
114
|
+
export declare function publicAuthorizer(staticInit?: RequestInit, options?: PublicAuthorizerOptions): RequestAuthorizer;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Options for {@link fetchBinaryAsset}. */
|
|
2
|
+
export interface FetchBinaryAssetOptions extends RequestInit {
|
|
3
|
+
/**
|
|
4
|
+
* Label for the thing being fetched, used in the error message (e.g. the
|
|
5
|
+
* layer or codec name). Defaults to no prefix.
|
|
6
|
+
*/
|
|
7
|
+
readonly label?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Fetch `url` and return its bytes, rejecting anything that is plainly not a
|
|
11
|
+
* binary asset.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* Codecs identify their input by a magic number, so when a URL 404s to an HTML
|
|
15
|
+
* fallback the decoder reports a *bad magic number* — for HTML it was never
|
|
16
|
+
* meant to read. That error names the codec, not the missing file, and reliably
|
|
17
|
+
* sends the reader off to debug a decoder that is working correctly. (A real
|
|
18
|
+
* example: `BadMagic { found: 1868833084 }` — which is the ASCII `<!do` of
|
|
19
|
+
* `<!doctype html>`.)
|
|
20
|
+
*
|
|
21
|
+
* This checks the response status, then the content type, then sniffs the
|
|
22
|
+
* leading bytes when the server sent no usable type — so the failure names the
|
|
23
|
+
* URL and what came back instead.
|
|
24
|
+
*
|
|
25
|
+
* @param url - The asset URL.
|
|
26
|
+
* @param opts - Standard `RequestInit` (pass `signal` to make it abortable), plus an optional `label`.
|
|
27
|
+
* @returns The response body as bytes.
|
|
28
|
+
* @throws BitruviusError `BVX_ASSET_FETCH_FAILED` when the response is an HTTP
|
|
29
|
+
* error, or is textual rather than binary.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { fetchBinaryAsset } from '@bitruvius/foundation';
|
|
34
|
+
*
|
|
35
|
+
* const bytes = await fetchBinaryAsset('/assets/scene.bvc', { signal, label: 'BvcMapLibreLayer' });
|
|
36
|
+
* const model = await decoder.open(bytes);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function fetchBinaryAsset(url: string, opts?: FetchBinaryAssetOptions): Promise<Uint8Array>;
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bitruvius/foundation",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Bitruvius SDK foundation: the domain-agnostic, geo-free base every Bitruvius SDK builds on (task/worker scheduling, runtime capability detection, request authorization, SDK config, and error types).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/bitruvius/support"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://dev.bitruvius.com/sdks/js/",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"bitruvius",
|
|
13
|
+
"sdk",
|
|
14
|
+
"foundation",
|
|
15
|
+
"scheduling",
|
|
16
|
+
"worker-pool"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": [
|
|
19
|
+
"./src/index.ts"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "bun build ./src/index.ts --outdir dist --format esm --target browser --minify && tsc --emitDeclarationOnly",
|
|
44
|
+
"test": "bun test"
|
|
45
|
+
}
|
|
46
|
+
}
|