@plasius/gpu-model-runtime 0.2.5 → 0.3.1
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 +47 -6
- package/dist/index.cjs +34 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -61
- package/dist/index.d.ts +12 -61
- package/dist/index.js +34 -30
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -7,8 +7,21 @@ This repository is the dedicated package boundary defined by ADR 0094.
|
|
|
7
7
|
|
|
8
8
|
The runtime owns orchestration only. Canonical model documents, diagnostics, and
|
|
9
9
|
renderer-specific data remain owned by the corresponding `@plasius/gpu-model-*`
|
|
10
|
-
packages
|
|
11
|
-
|
|
10
|
+
packages. Published `@plasius/gpu-model-core@0.6.0` supplies source, provenance,
|
|
11
|
+
diagnostic-mode and canonical document types; runtime `ModelSource` adds the
|
|
12
|
+
optional package-resource descriptor. Existing explicit generic model types
|
|
13
|
+
remain supported. `ModelAdapter`, `AdapterLoadResult` and `ModelLoadResult` now
|
|
14
|
+
have `GpuModelDocument` as their optional type default. `loadModel<T>()` remains
|
|
15
|
+
explicitly generic; an unannotated call still returns an unknown model.
|
|
16
|
+
|
|
17
|
+
Core imports in runtime source are type-only, so importing this runtime does not
|
|
18
|
+
execute core validators or eagerly load concrete adapters. Adapters own canonical
|
|
19
|
+
validation and strict/tolerant/forensic policy through the core factories and
|
|
20
|
+
evaluator. Runtime returns the resulting document and diagnostic ledger without
|
|
21
|
+
rewriting them. Its `ModelAdapter` is an acquisition/worker port, distinct from
|
|
22
|
+
core's inspect/load/export/validate conversion interface: bind a format adapter's
|
|
23
|
+
factory to the provided resource resolver and map its result in that port;
|
|
24
|
+
do not cast a format module directly to a runtime adapter.
|
|
12
25
|
|
|
13
26
|
## Runtime behavior
|
|
14
27
|
|
|
@@ -25,23 +38,28 @@ is released.
|
|
|
25
38
|
statuses, abort signals, optional byte ranges, response content types, and
|
|
26
39
|
SHA-256 integrity checks.
|
|
27
40
|
- `MemoryModelCache` keys loaded output by content hash, adapter format, and
|
|
28
|
-
stable adapter options
|
|
29
|
-
|
|
41
|
+
stable adapter options, acquisition byte ceiling, diagnostic mode and actual
|
|
42
|
+
execution path. Call `runtime.invalidate()` to clear all entries or entries
|
|
43
|
+
for one content hash.
|
|
30
44
|
- Worker execution is opt-in through a `WorkerDispatcher`; consumers that do
|
|
31
45
|
not provide one stay on the main execution path.
|
|
32
46
|
|
|
33
47
|
Example:
|
|
34
48
|
|
|
35
49
|
```ts
|
|
50
|
+
import { AdapterRegistry, ModelRuntime, type GpuModelDocument } from "@plasius/gpu-model-runtime";
|
|
51
|
+
|
|
36
52
|
const registry = new AdapterRegistry().register({
|
|
37
53
|
formatId: "gltf",
|
|
38
54
|
extensions: ["gltf", "glb"],
|
|
39
55
|
sniff: ({ bytes }) => bytes[0] === 0x67,
|
|
40
|
-
|
|
56
|
+
// A consumer-owned bridge implementing this runtime's ModelAdapter port.
|
|
57
|
+
// Its factory binds the format adapter to input.resourceResolver.
|
|
58
|
+
load: () => import("./model-adapters/gltf.js"),
|
|
41
59
|
});
|
|
42
60
|
|
|
43
61
|
const runtime = new ModelRuntime({ registry });
|
|
44
|
-
const result = await runtime.loadModel({
|
|
62
|
+
const result = await runtime.loadModel<GpuModelDocument>({
|
|
45
63
|
kind: "blob-storage-url",
|
|
46
64
|
url: "https://storage.example.invalid/models/scene.glb?<signed-url>",
|
|
47
65
|
});
|
|
@@ -50,6 +68,22 @@ result.canonicalModel;
|
|
|
50
68
|
result.rendererReady;
|
|
51
69
|
```
|
|
52
70
|
|
|
71
|
+
A tolerant or forensic cache entry cannot satisfy a strict request. Mandatory
|
|
72
|
+
worker support is checked before cache lookup; main and worker results have
|
|
73
|
+
separate keys, while `auto` and `always` can share a result when both select the
|
|
74
|
+
worker. Adapters and dispatchers injected by the consumer are trusted execution
|
|
75
|
+
ports; they must honor signals, enforce their own parsing/decoding budgets, and
|
|
76
|
+
return core-validated documents. This package does not supply a Worker script or
|
|
77
|
+
serialize resolver functions across threads. A dispatcher binds its own RPC and
|
|
78
|
+
resource-resolution transport.
|
|
79
|
+
|
|
80
|
+
New cache keys intentionally cold-start existing external caches. This is a
|
|
81
|
+
patch-compatible policy fix with additive canonical type defaults and one exact
|
|
82
|
+
published dependency (core 0.6.0, previously absent); no concrete adapter is added.
|
|
83
|
+
The new dependency has no runtime dependencies and requires the already-supported
|
|
84
|
+
Node 24. Installed package and strict ESM/CJS declaration checks verify the
|
|
85
|
+
published boundary. Do not use local workspace links.
|
|
86
|
+
|
|
53
87
|
## Bounded source acquisition
|
|
54
88
|
|
|
55
89
|
Every entrypoint and related resource has a default ceiling of 64 MiB
|
|
@@ -77,6 +111,13 @@ underestimate never disables actual-byte checks. Hashes still cover the admitted
|
|
|
77
111
|
bytes, including a returned range. Limits apply to decoded Fetch body bytes;
|
|
78
112
|
compressed Content-Length is only an additional early check.
|
|
79
113
|
|
|
114
|
+
Integrity accepts a 64-character SHA-256 hex digest (case-insensitive) or its
|
|
115
|
+
standard padded base64 encoding (case-sensitive), with an optional `sha256-`
|
|
116
|
+
prefix. The same verifier applies to fetched and in-memory package resources;
|
|
117
|
+
malformed or mismatched digests fail without retry. Cross-realm `Uint8Array`
|
|
118
|
+
stream chunks are accepted with the same limits and copy ownership, while other
|
|
119
|
+
typed-array kinds, DataViews and spoofed byte-array tags are rejected.
|
|
120
|
+
|
|
80
121
|
Oversize throws `ModelResourceTooLargeError` with code
|
|
81
122
|
`MODEL_RESOURCE_TOO_LARGE` and a fixed message containing no source identifiers.
|
|
82
123
|
It is not retried. Cancellation and acquisition deadlines stop pending reads,
|
package/dist/index.cjs
CHANGED
|
@@ -74,6 +74,10 @@ function createModelCacheKey(contentHash, formatId, adapterOptions) {
|
|
|
74
74
|
var DEFAULT_MODEL_MAX_BYTES = 64 * 1024 * 1024;
|
|
75
75
|
|
|
76
76
|
// src/bytes.ts
|
|
77
|
+
var typedArrayKind = Object.getOwnPropertyDescriptor(
|
|
78
|
+
Object.getPrototypeOf(Uint8Array.prototype),
|
|
79
|
+
Symbol.toStringTag
|
|
80
|
+
)?.get;
|
|
77
81
|
var ModelResourceTooLargeError = class extends Error {
|
|
78
82
|
code = "MODEL_RESOURCE_TOO_LARGE";
|
|
79
83
|
constructor() {
|
|
@@ -150,7 +154,7 @@ async function readBoundedStream(source, maxBytes, signal) {
|
|
|
150
154
|
const next = await abortable(reader ? reader.read() : iterator.next(), signal);
|
|
151
155
|
checkAbort(signal);
|
|
152
156
|
if (next.done) break;
|
|
153
|
-
if (!(next.value
|
|
157
|
+
if (!ArrayBuffer.isView(next.value) || typedArrayKind?.call(next.value) !== "Uint8Array") throw new TypeError("Model stream chunks must be Uint8Array");
|
|
154
158
|
const chunk = next.value;
|
|
155
159
|
if (chunk.byteLength > maxBytes - length) throw new ModelResourceTooLargeError();
|
|
156
160
|
const required = length + chunk.byteLength;
|
|
@@ -282,12 +286,15 @@ async function verifyIntegrity(bytes, integrity, hashBytes) {
|
|
|
282
286
|
if (!integrity) {
|
|
283
287
|
return;
|
|
284
288
|
}
|
|
285
|
-
const expected = integrity.replace(/^sha256-/iu, "")
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
289
|
+
const expected = integrity.replace(/^sha256-/iu, "");
|
|
290
|
+
if (/^[0-9a-f]{64}$/iu.test(expected)) {
|
|
291
|
+
const actualHex = await hashBytes(bytes);
|
|
292
|
+
if (expected.toLowerCase() === actualHex.toLowerCase()) return;
|
|
293
|
+
} else if (/^[A-Za-z0-9+/]{43}=$/u.test(expected)) {
|
|
294
|
+
const actualBase64 = bytesToBase64(await digestBytes(bytes, hashBytes));
|
|
295
|
+
if (expected === actualBase64) return;
|
|
290
296
|
}
|
|
297
|
+
throw new NonRetryableResourceError("Model resource integrity check failed");
|
|
291
298
|
}
|
|
292
299
|
async function digestBytes(bytes, hashBytes) {
|
|
293
300
|
if (globalThis.crypto?.subtle) {
|
|
@@ -377,7 +384,7 @@ var PackageResourceResolver = class {
|
|
|
377
384
|
const memoryResource = this.memoryResources[memoryKey] ?? this.memoryResources[`./${memoryKey}`];
|
|
378
385
|
if (memoryResource) {
|
|
379
386
|
const bytes = await boundedBytes(memoryResource.bytes, maxBytes, controller.signal);
|
|
380
|
-
await abortable(
|
|
387
|
+
await abortable(verifyIntegrity(bytes, requested.integrity ?? memoryResource.integrity, this.hashBytes), controller.signal);
|
|
381
388
|
return {
|
|
382
389
|
path: memoryKey,
|
|
383
390
|
bytes,
|
|
@@ -465,7 +472,23 @@ var ModelRuntime = class {
|
|
|
465
472
|
fileName: input.fileName,
|
|
466
473
|
sourceKind: source.kind
|
|
467
474
|
}, options.formatId);
|
|
468
|
-
const
|
|
475
|
+
const workerMode = options.worker?.mode ?? "auto";
|
|
476
|
+
const canUseWorker = workerMode !== "never" && Boolean(this.workerDispatcher) && Boolean(adapter.supportsWorker);
|
|
477
|
+
if (workerMode === "always" && !canUseWorker) {
|
|
478
|
+
throw new Error(`Adapter ${adapter.formatId} does not support the requested worker execution`);
|
|
479
|
+
}
|
|
480
|
+
const context = {
|
|
481
|
+
signal: options.signal ?? new AbortController().signal,
|
|
482
|
+
mode: options.mode ?? "strict",
|
|
483
|
+
execution: canUseWorker ? "worker" : "main",
|
|
484
|
+
adapterOptions: options.adapterOptions ?? null
|
|
485
|
+
};
|
|
486
|
+
const cacheKey = createModelCacheKey(input.contentHash, adapter.formatId, {
|
|
487
|
+
adapterOptions: context.adapterOptions,
|
|
488
|
+
maxBytes: byteLimit(options.fetchPolicy?.maxBytes),
|
|
489
|
+
mode: context.mode,
|
|
490
|
+
execution: context.execution
|
|
491
|
+
});
|
|
469
492
|
const useCache = options.useCache ?? true;
|
|
470
493
|
if (useCache) {
|
|
471
494
|
const cached = await this.cache.get(cacheKey);
|
|
@@ -473,13 +496,7 @@ var ModelRuntime = class {
|
|
|
473
496
|
return { ...cached, fromCache: true };
|
|
474
497
|
}
|
|
475
498
|
}
|
|
476
|
-
const
|
|
477
|
-
signal: options.signal ?? new AbortController().signal,
|
|
478
|
-
mode: options.mode ?? "strict",
|
|
479
|
-
execution: "main",
|
|
480
|
-
adapterOptions: options.adapterOptions ?? null
|
|
481
|
-
};
|
|
482
|
-
const adapterResult = await this.loadWithExecution(adapter, input, context, options);
|
|
499
|
+
const adapterResult = await this.loadWithExecution(adapter, input, context);
|
|
483
500
|
const result = {
|
|
484
501
|
...adapterResult,
|
|
485
502
|
contentHash: input.contentHash,
|
|
@@ -494,13 +511,8 @@ var ModelRuntime = class {
|
|
|
494
511
|
}
|
|
495
512
|
return result;
|
|
496
513
|
}
|
|
497
|
-
async loadWithExecution(adapter, input, context
|
|
498
|
-
|
|
499
|
-
const canUseWorker = workerMode !== "never" && Boolean(this.workerDispatcher) && Boolean(adapter.supportsWorker);
|
|
500
|
-
if (workerMode === "always" && !canUseWorker) {
|
|
501
|
-
throw new Error(`Adapter ${adapter.formatId} does not support the requested worker execution`);
|
|
502
|
-
}
|
|
503
|
-
if (canUseWorker && this.workerDispatcher) {
|
|
514
|
+
async loadWithExecution(adapter, input, context) {
|
|
515
|
+
if (context.execution === "worker" && this.workerDispatcher) {
|
|
504
516
|
const workerRequest = {
|
|
505
517
|
formatId: adapter.formatId,
|
|
506
518
|
input,
|
|
@@ -606,14 +618,6 @@ async function sourceToBytes(source, maxBytes, signal) {
|
|
|
606
618
|
if (source.byteLengthHint !== void 0) checkSize(source.byteLengthHint, maxBytes);
|
|
607
619
|
return readBoundedStream(source.stream, maxBytes, signal);
|
|
608
620
|
}
|
|
609
|
-
async function verifyResourceIntegrity(bytes, integrity, hashBytes) {
|
|
610
|
-
if (!integrity) return;
|
|
611
|
-
const expected = integrity.replace(/^sha256-/iu, "").toLowerCase();
|
|
612
|
-
const actual = (await hashBytes(bytes)).toLowerCase();
|
|
613
|
-
if (actual !== expected) {
|
|
614
|
-
throw new Error("Model resource integrity check failed");
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
621
|
var defaultFetch = (input, init) => {
|
|
618
622
|
if (!globalThis.fetch) {
|
|
619
623
|
throw new Error("URL sources require fetch or an injected fetch dependency");
|