@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 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 and are carried through generic type parameters until the core package
11
- is released.
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 and acquisition byte ceiling. Call `runtime.invalidate()` to clear all entries or
29
- entries for one content hash.
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
- load: () => import("@plasius/gpu-model-gltf"),
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 instanceof Uint8Array)) throw new TypeError("Model stream chunks must be Uint8Array");
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, "").toLowerCase();
286
- const actualHex = (await hashBytes(bytes)).toLowerCase();
287
- const actualBase64 = bytesToBase64(await digestBytes(bytes, hashBytes));
288
- if (expected !== actualHex && expected !== actualBase64) {
289
- throw new NonRetryableResourceError("Model resource integrity check failed");
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(verifyResourceIntegrity(bytes, requested.integrity ?? memoryResource.integrity, this.hashBytes), controller.signal);
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 cacheKey = createModelCacheKey(input.contentHash, adapter.formatId, { adapterOptions: options.adapterOptions ?? null, maxBytes: byteLimit(options.fetchPolicy?.maxBytes) });
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 context = {
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, options) {
498
- const workerMode = options.worker?.mode ?? "auto";
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");