Skip to content

feat(resources): redesign asset descriptors and type resolution - #420

Merged
Exoridus merged 13 commits into
mainfrom
feat/asset-system-descriptor-redesign
Jul 27, 2026
Merged

feat(resources): redesign asset descriptors and type resolution#420
Exoridus merged 13 commits into
mainfrom
feat/asset-system-descriptor-redesign

Conversation

@Exoridus

Copy link
Copy Markdown
Owner

Summary

  • vereinheitlicht Asset-Deskriptoren um type, ctor und Asset.type(...)
  • führt explizite app-lokale Extension-Type-Overrides ein
  • trennt Benutzer-Overrides von binding-deklarierten Defaults
  • entfernt den alten öffentlichen Loader.register()-/AssetFactory-Pfad
  • konsolidiert get() und load() auf die kanonische Descriptor-Normalisierung
  • präzisiert State-, Retry- und Error-Semantik
  • migriert Guides, Extensions, Beispiele, Type-Tests und API-Dokumentation

Resolution precedence

  1. expliziter app-lokaler registerType()-Override
  2. binding-deklarierter app-lokaler Default
  3. globaler defineAsset-Default
  4. verständlicher Fehler

Important behavior

  • explizite Overrides sind pro Loader/Application isoliert
  • identische Wiederholungen sind idempotent
  • widersprüchliche explizite Overrides werden abgelehnt
  • spätere Bindings verdrängen keinen expliziten Override
  • Binding-Konflikte werden atomar vor Mutation erkannt
  • Asset.type(...) wirkt nur auf den jeweiligen Aufruf
  • get() meldet asynchrone Fehler über Zustand und onError
  • load() rejected und meldet denselben Fehler über onError
  • unterstützte Retries können fehlgeschlagene Handles heilen

Test plan

  • pnpm typecheck
  • pnpm typecheck:type-tests
  • pnpm docs:api:check
  • pnpm verify:quick
  • pnpm test
  • gezielte AssetTypeRegistry- und Loader-Extension-Matching-Tests: 2 Dateien, 36 Tests bestanden
  • vollständiger Lauf: 454 Testdateien bestanden, 1 übersprungen; 8.247 Tests bestanden, 16 übersprungen, 0 fehlgeschlagen

Review note

Der Branch wurde nach Abschluss eines Whole-Branch-Reviews auf aktuelles main (bef47f39) rebased. PR #419 war dort als Squash-Commit integriert; die Patch-ID des Squash-Commits entspricht exakt der ursprünglichen zweigliedrigen SystemRegistry-Serie. Die 13 Asset-System-Commits wurden konfliktfrei und ohne Reordering auf diesen Stand gesetzt.

Lokal bestehen keine bekannten offenen Critical-, High- oder Medium-Findings.

Exoridus added 13 commits July 27, 2026 02:41
…criptor surface

DefineAssetDescriptor/AssetBinding's constructor-reference field is now `ctor`
(was `type`), and the string kind-tag field is now `type` (was `kind`), across
Asset/AssetImpl, AnyAssetConfig, AssetTypeRegistry.bindAsset, defineAsset,
coreAssetBindings, and every consumer (Loader, Assets, AssetResidency,
materializeAssetBindings). Foundation task for the asset-loading API redesign.
… guides, examples, api docs

Task 1's descriptor rename left packages/exojs-{tiled,aseprite,ldtk} on the old
DefineAssetDescriptor/AssetBinding shape, breaking pnpm verify:quick: package
typecheck errors, guide/example snippets still calling Asset.kind(...), and a
TypeDoc entry-point failure that aborted API-doc generation. Applies the same
two renames (string kind-tag -> type, constructor reference -> ctor) across
the three packages' src/ and test/, their READMEs, every affected guide MDX
and top-level example (.ts + regenerated .js), and regenerates site/src/content/api.

Also fixes a real runtime bug uncovered by running the package test suites:
several test mocks branched on a type-erased `asset.kind` check that the
rename silently broke (asset._config now carries `type`, not `kind`).
…tTypeRegistry

AssetTypeRegistry.registerType()/resolveExtensionType() let a Loader override
which AssetDefinitions type a file extension resolves to, app-local and
override-first ahead of the global extensionKindRegistry default. bindAsset()
now accepts an optional `type` and writes its extensions into the same
override table so defineAsset-based bindings and explicit registerType()
calls conflict-check consistently. Loader.registerType() exposes it publicly.
…terType coverage

AssetBinding.type's JSDoc claimed materializeAssetBindings doesn't consume it,
which the Task 2 wiring in materialize.ts already contradicted — it now
forwards type into Loader.bindAsset to populate the per-app extension
override table. Also notes the same on AssetTypeRegistry.bindAsset's
docstring and adds a Loader.registerType() pass-through unit test.
… registration path

Loader.register()/AssetFactory were a parallel, unused registration path
alongside bindAsset()/defineAsset() (the paved path since the asset-system
v2 rework) — every core binding already went through bindAsset via
coreAssetBindings.ts, so the factory-fallback dispatch in AssetDecoder was
dead for first-party types.

Removes Loader.register(), AssetDecoder._fetch() and its
hasFactory/resolveFactory fallback branch in _dispatchFetch/_injectSource,
and AssetTypeRegistry's register()/hasFactory()/resolveFactory()/
destroyFactories(). FactoryRegistry.ts/AssetFactory.ts/AbstractAssetFactory.ts
and the concrete factories/*.ts implementations stay — they're still used
internally by coreAssetBindings.ts's binaryFactoryHandler/textFactoryHandler.

Closes the one real capability gap register() had — a per-type IDB cache
namespace — by adding an optional storageName to defineAsset/bindAsset,
threaded through AssetBinding -> Loader.bindAsset -> AssetTypeRegistry's
HandlerEntry -> AssetDecoder's context.fetchText/fetchArrayBuffer/fetchJson
namespace selection.

test/resources/loader.test.ts's register()-based TextAsset fixture is
replaced with a bindAsset handler routed through context.fetchText; the
corrupt-cache-entry delete+retry behavior it incidentally covered (now
unreachable through the context.fetchText identity-factory path) gets its
own direct CacheFirstStrategy unit tests in the new
test/resources/cache-first-strategy.test.ts.
…no-handler paths

Task review found _loadSingleAsset's two new behaviors from the AssetResidency
_fetch() fix (storageName threaded into _buildHandlerContext, and rejecting
with a clear error instead of the removed factory fallback when no handler is
bound) had no test coverage, unlike the identical logic in AssetDecoder.
_dispatchFetch. Adds the two missing tests, mirroring asset-decoder.test.ts's
existing pattern.

Also: extracted the duplicated "No asset handler registered..." error string
(built independently in AssetDecoder and AssetResidency) into a shared
AssetTypeRegistry._missingHandlerError(type) helper, and fixed a stale
Loader.hasLoadable() doc comment that still said "handler or factory" after
the factory path was removed.
…input shape

Bare paths now normalize to a canonical { type, source } descriptor through
AssetTypeRegistry._resolveTypeForPath, which walks the basename's dot-suffixes
longest-first and consults the app-local registerType override before the global
defineAsset default. get()/load() drop from seven overloads each to five: the
two-argument constructor lookup form and the ExtensionTypeMap/LoadByPath-based
path overloads are gone, leaving one bare-path signature typed through
KindByPath/LeafForPath.

- Loader.get(Type, alias) and its SceneLoader mirror are removed; the residency
  read accessor _getStored that only backed it goes with them.
- load('x.png') now infers Texture instead of unknown (the legacy generic
  overload used to win and erase the return type).
- Non-leaf resource types (bmFont, font, svg, image, music, video) no longer
  accept a bare path at compile time, matching get() and Assets.from(); name
  them with Asset.type(...). The runtime still resolves them for dynamic strings.
- An extension bound via bindAsset without a type reserves the suffix but no
  longer participates in bare-path resolution.
…ndMap

Task 4 review follow-up (two Important findings).

1. exojs-tiled and exojs-ldtk augmented ExtensionTypeMap, which no longer feeds
   any call signature after the get()/load() consolidation — the surviving
   bare-path overload reads KindByPath -> ExtensionKindMap. Both packages'
   documented shorthands (load('world.tmj') / load('world.ldtk')) therefore
   resolved to a never parameter for consumers. Runtime was unaffected: bindAsset
   already writes each binding's extensions into the app override table.

   ExtensionKindMap and KindByPath were not exported from the root index, so the
   augmentation the new JSDoc tells consumers to write was not actually
   reachable; both are exported now.

   Pinned by test/type-tests/package-bare-path-inference.type-test.ts, compiled
   by typecheck:type-tests. Package test/ dirs are excluded from their own
   tsconfig, so an assertion placed there would never have been gated; verified
   by reverting the tiled augmentation and confirming the gate fails with
   "Argument of type '\"world.tmj\"' is not assignable to parameter of type 'never'".

2. The rewritten non-leaf bare-path branch (font family inference from the
   filename, bmFont dispatch) had no coverage through Loader.load(path), since
   those suffixes are never at the type level. Added four tests using the same
   'as never' cast the rest of the file uses; verified the font assertions fail
   when the family-inference condition is mutated away.
…kind

Task 4 review follow-up round 2.

Round 1 moved tmj/ldtk into ExtensionKindMap, which correctly fixed
load('world.tmj') but also made those suffixes reachable through get()'s
bare-path overload. LeafForPath decides whether to wrap the leaf in AssetRef by
testing membership in ValueAssetKind, which was a hardcoded 8-member union of
core kinds — so get('world.tmj') typed as bare TileMap while defineAsset's
runtime default (isValue ?? seamless === undefined) makes it an AssetRef<TileMap>.
A consumer calling loader.get('world.tmj').someMethod() compiled and crashed.

ValueAssetKind is now CoreValueAssetKind (the unchanged 8 built-ins) unioned
with the kinds a declaration-merged AssetDefinitions entry marks isValue: true.
The marker is co-located with the resource/config it describes and named after
the defineAsset option it mirrors, so no new exported interface or second
augmentation block is needed — ValueAssetKind is not exported from the root, so
nothing about the public surface changed.

Loader._valueTokenByKind is re-keyed to CoreValueAssetKind. It maps kinds to
built-in dispatch tokens that only exist for core types, and keeping it on the
now-extensible union would have made every build that sees a package
augmentation demand a token that cannot exist.

tileMap, tiledMap, ldtkMap and asepriteSheet all ship without a seamless
adapter, so all four are marked. Verified fail-before/pass-after by dropping the
tileMap marker: the get() and catalog-leaf assertions fail while the load()
assertion still passes, which is the correct split. Two runtime tests pin the
defineAsset default the marker mirrors, so the two halves cannot drift silently.

Also adds the missing @codexo/exojs-aseprite path mapping to
tsconfig.examples.json, without which the aseprite assertions could not resolve.
…document onError as the production diagnostic contract
…th a seeded-random fuzz test

The three static tests duplicated coverage already in loader-claims.test.ts,
asset-ref.test.ts, loader-seamless.test.ts, and loader.test.ts, and never
exercised the idle readiness state or true claim/fetch interleaving. Replaced
with one SG-006-style seeded PRNG walk (200 steps) over two disjoint key
channels — get()/_adopt claim residency and load() store-before-fetch dedup —
checking claim-count/eviction and store/fetch invariants after every step.
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 15.66kB (0.06%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
exo-esm-esm 876.22kB 1.56kB (0.18%) ⬆️
exo-esm-modules-esm 3.98MB 8.75kB (0.22%) ⬆️
exo-iife-min-Exo-iife 883.96kB 685 bytes (0.08%) ⬆️
exojs-aseprite-esm 27.53kB 53 bytes (0.19%) ⬆️
exo-full-iife-Exo-iife 2.82MB 4.29kB (0.15%) ⬆️
exojs-ldtk-esm 58.83kB 128 bytes (0.22%) ⬆️
exojs-tiled-esm 130.03kB 192 bytes (0.15%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: exo-esm-modules-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
resources/AssetResidency.js -612 bytes 48.96kB -1.23%
resources/Loader.js 2.53kB 35.43kB 7.7% ⚠️
resources/Loader.d.ts -548 bytes 28.04kB -1.92%
resources/AssetTypeRegistry.js 4.29kB 13.38kB 47.27% ⚠️
resources/AssetResidency.d.ts -443 bytes 12.8kB -3.35%
rendering/text/BitmapText.js 20 bytes 9.72kB 0.21%
resources/AssetDefinitions.d.ts 1.9kB 9.57kB 24.79% ⚠️
extensions/Extension.d.ts 490 bytes 8.95kB 5.79% ⚠️
resources/AssetTypeRegistry.d.ts 2.99kB 8.1kB 58.5% ⚠️
rendering/text/Text.js 7 bytes 7.87kB 0.09%
rendering/text/TextStyle.d.ts 7 bytes 7.01kB 0.1%
resources/AssetDecoder.js -793 bytes 6.81kB -10.42%
resources/AssetDecoder.d.ts 113 bytes 4.89kB 2.37%
rendering/text/Text.d.ts 7 bytes 4.5kB 0.16%
rendering/text/BitmapText.d.ts 20 bytes 4.24kB 0.47%
extensions/materialize.js 426 bytes 4.24kB 11.18% ⚠️
resources/index.d.ts -80 bytes 3.53kB -2.21%
resources/defineAsset.d.ts 214 bytes 3.09kB 7.45% ⚠️
resources/seamless.d.ts -6 bytes 2.83kB -0.21%
resources/tokens.js -338 bytes 2.47kB -12.02%
resources/extensionKindRegistry.js 319 bytes 2.29kB 16.19% ⚠️
resources/defineAsset.js 94 bytes 2.08kB 4.74%
resources/assetKindRegistry.js 98 bytes 2.05kB 5.02% ⚠️
core/scene/SceneLoader.d.ts -223 bytes 1.98kB -10.13%
resources/CacheStrategy.d.ts 137 bytes 1.93kB 7.66% ⚠️
resources/CacheFirstStrategy.js -23 bytes 1.9kB -1.2%
rendering/text/BmFont.d.ts -105 bytes 1.72kB -5.76%
resources/FactoryRegistry.d.ts 116 bytes 1.66kB 7.51% ⚠️
resources/extensionKindRegistry.d.ts 167 bytes 1.58kB 11.84% ⚠️
resources/CacheFirstStrategy.d.ts -23 bytes 988 bytes -2.27%
resources/NetworkOnlyStrategy.js -3 bytes 978 bytes -0.31%
resources/assetKindRegistry.d.ts 14 bytes 938 bytes 1.52%
core/scene/SceneLoader.js -8 bytes 919 bytes -0.86%
resources/NetworkOnlyStrategy.d.ts -3 bytes 774 bytes -0.39%
rendering/text/BmFont.js -105 bytes 758 bytes -12.17%
resources/FactoryRegistry.js (Deleted) -1.91kB 0 bytes -100.0% 🗑️
view changes for bundle: exo-iife-min-Exo-iife

Assets Changed:

Asset Name Size Change Total Size Change (%)
exo.iife.min.js 685 bytes 883.96kB 0.08%
view changes for bundle: exojs-tiled-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
public.d.ts 162 bytes 2.94kB 5.82% ⚠️
tiledRuntimeMapBinding.js 15 bytes 1.67kB 0.91%
tiledRuntimeMapBinding.d.ts 15 bytes 1.13kB 1.35%
view changes for bundle: exojs-ldtk-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
public.d.ts 50 bytes 1.72kB 3.0%
ldtkBinding.js 39 bytes 1.02kB 3.96%
ldtkBinding.d.ts 39 bytes 706 bytes 5.85% ⚠️
view changes for bundle: exo-esm-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
exo.esm.js 1.56kB 876.22kB 0.18%
view changes for bundle: exo-full-iife-Exo-iife

Assets Changed:

Asset Name Size Change Total Size Change (%)
exo.full.iife.js 4.29kB 2.82MB 0.15%
view changes for bundle: exojs-aseprite-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
asepriteBinding.js 13 bytes 4.8kB 0.27%
asepriteBinding.d.ts 13 bytes 961 bytes 1.37%
public.d.ts 27 bytes 767 bytes 3.65%

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.55556% with 6 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/resources/Loader.ts 95.55% 2 Missing ⚠️
src/extensions/materialize.ts 87.50% 0 Missing and 1 partial ⚠️
src/resources/AssetResidency.ts 91.66% 1 Missing ⚠️
src/resources/assetKindRegistry.ts 66.66% 1 Missing ⚠️
src/resources/defineAsset.ts 75.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@Exoridus
Exoridus marked this pull request as ready for review July 27, 2026 00:58
@Exoridus
Exoridus enabled auto-merge (squash) July 27, 2026 00:58
@Exoridus
Exoridus merged commit 0f4b57e into main Jul 27, 2026
18 checks passed
@Exoridus
Exoridus deleted the feat/asset-system-descriptor-redesign branch July 27, 2026 01:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant