Skip to content

feat(resources)!: asset-access redesign — Asset.kind, kind discriminator, idle/awaitBackground, Assets.one/group + lifecycle merge#267

Merged
Exoridus merged 86 commits into
mainfrom
feat/v0.16-loader-access-redesign
Jul 10, 2026
Merged

feat(resources)!: asset-access redesign — Asset.kind, kind discriminator, idle/awaitBackground, Assets.one/group + lifecycle merge#267
Exoridus merged 86 commits into
mainfrom
feat/v0.16-loader-access-redesign

Conversation

@Exoridus

@Exoridus Exoridus commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Finalizes the asset-access redesign into a single, catalog-centric surface and
folds in the loader-lifecycle slice. Builds on S1–S3 (catalog handles,
defineAsset, status channel) and applies the post-S3 finalization delta.

Final public API

// Declaration + access
const LevelAssets = Assets.from({
  player: 'sprites/player.png',                    // bare path → Texture (kind inferred)
  config: Asset.kind<LevelConfig>('json', 'levels/01.json'),
  map:    Asset.kind('tileMap', 'levels/01.tmj'),
  ...Assets.group('sound', { jump: 'jump.wav', hit: 'hit.wav' }),
});
const chunk = Assets.one({ kind: 'json', source: 'chunk.json', parse: parseChunk });

// Loader verbs
await loader.load(LevelAssets);              // resolved value map; catalog props stay usable
loader.load(NextLevel, { background: true });
await loader.awaitBackground();
loader.unload(LevelAssets);

What changed (finalization delta, per-slice)

  • A Asset.kind() — the single typed descriptor builder (kind autocomplete from AssetDefinitions, resource inference from kind, kind-specific options, <T> only for value kinds).
  • B Config discriminator typekind (pre-1.0 clean break — no shim).
  • C New idle load state for unadopted catalog leaves (usable placeholder; not for manually-constructed resources).
  • D load(catalog) returns a resolved value map (refs unwrapped).
  • E loadAll()awaitBackground().
  • F/G Assets.one() and Assets.group() (spread form).
  • H1a Value-brand: Asset.kind<Config>('json', …) classifies as AssetRef<Config> in a catalog (object-valued JSON no longer misread as a resource).
  • H1b parse post-load transform (type + runtime; per-leaf, applied on fill).
  • H2–H4 Migrated engine, tests, ~110 examples and the guides/API-docs onto the new surface.
  • H5 Merged feat/v0.16-loader-lifecycle — examples move off Scene.load()/unload() hooks to init() while keeping the final asset API; adds a SceneLoader.get(asset) overload mirroring Loader.get.
  • I Deleted the .of() statics everywhere (core + token + extension classes), internalized _makeAsset, removed the inline record-catalog load overload, and marked typeNames @internal. Asset.kind is the sole descriptor builder.

Breaking changes (pre-1.0)

  • { type, source }{ kind, source }.
  • X.of(src)Asset.kind('<kind>', src) (e.g. AudioStream.ofAsset.kind('music', …)).
  • loader.load(Type, src) / loader.get(Type, src) fetch-by-token forms removed. A legacy in-memory get(type, alias) lookup (a cache read, not a fetch) remains for non-seamless / loadContainer assets — marked @advanced.
  • The typed inline record-catalog load({ a, b }) overload is removed — build catalogs with Assets.from({ a, b }). A runtime record fallback is retained as an internal-legacy path (multi-alias/identity plumbing + its coverage), so this is not a hard runtime removal.
  • loadAll()awaitBackground().

Verification

  • verify:quick green (typecheck + guides + examples + type-tests + packages + lint:all + format:check + docs:api:check).
  • Full suite: 7024 passed, 0 failed.
  • Zero-hit grep across the repo confirms the clean break (no .of / loadAll / { type } / token forms remain).

Deferred / notes

  • Assets.group nested-field form (design §12 nice-to-have); the spread form covers the migration.
  • load({ a, b }) is now a runtime error (falls to the generic leaf overload), not a compile error — a true type error needs a meta-brand on the leaf overload.
  • Example smoke (site:build) not run here: it needs the built engine dist + vendor sync (environmental); example correctness is covered by typecheck:examples/typecheck:guides.
  • parse is synchronous-only in v1 (maps the decoded raw value; it may not return a Promise). Async parse is a follow-up — it would need the fill/store flow to await.
  • A follow-up loader-hardening slice tracks three pre-existing (S1–S3) failure-path gaps surfaced by review: a failed-shared-fetch join hang, a failed-catalog-leaf get() reget no-op vs the documented retry, and swallowed createLeaf error specificity.

Exoridus added 30 commits July 7, 2026 20:21
Migrate audio-basics, audio-fx, beat-detection, and spatial-audio
examples from Scene.load()/unload() to async init(loader) + seamless
this.loader.get()/load().

- Sound is seamless: resolved via the extension-inferred path-only
  get(source) form, which sidesteps a compile-time overload ambiguity
  between the Sound and Json tokens (both have zero-arg-constructible
  instance types) — the Sound token form still works but needs a cast.
- AudioStream has no seamless adapter: kept on awaited
  this.loader.load(AudioStream, { alias: source }) in async init. The
  single-path form of load() is rejected at compile time for .ogg
  because AudioStream isn't in ExtensionTypeMap for that extension, so
  the record/map form is used instead.
- spatial-audio scenes construct a derived Sound from .audioBuffer
  synchronously, so they await load(Sound, source) (cast to Sound)
  rather than the deferred get(), whose placeholder audioBuffer is
  null until the fetch fills it.
…le assertion

Rename assetMeta/stampMeta/readMeta to _assetMeta/_stampMeta/_readMeta to
follow the repo's internal-export convention. Replace the vacuous
Object.keys() non-enumerability check with a real descriptor assertion,
and note that the stamp's immutability is intentional.
…ation

Adds a loader-independent registry mapping type-string -> { adapter?, isValue }
plus registerAssetKind/getAssetKind/createLeaf, so Assets.from() can build a
meta-stamped placeholder handle (resource) or AssetRef (value) without ever
touching a Loader instance. Registers the core kinds (texture, sound as
resource kinds; json, text, csv, xml, srt, vtt, binary, wasm as value kinds)
as a side effect of loading seamless.ts.
…hten guard

Add coverage for the two reachable throw paths (registerAssetKind
conflict, createLeaf unregistered kind) and tighten the conflict guard
in registerAssetKind to also reject a re-registration that flips
isValue while keeping the same adapter.
…es in place

Adds Loader._adopt(handle, claimer): registers an externally-created
handle-hybrid leaf (a placeholder Texture/Sound or an AssetRef, already
meta-stamped by createLeaf) under its normalized _key, claims it, and
drives the fetch through the existing _deferred/_refs bookkeeping so the
single _storeResource fill site transplants the fetched payload into that
exact object. Every consumer holding the leaf pops in place.
…rs handleKey

Loader._adopt's already-stored branch (resource loaded/stored elsewhere
before a leaf is adopted, or a value already stored via load() with no
_refs entry yet) only called _claim() and never filled the adopted
handle nor registered it in _handleKeys. The adopted leaf hung in
'loading' forever, and release(handle) silently couldn't resolve its
key (claim leak).

Fix mirrors _getSeamless/_getRef's stored-fast-path exactly: fill the
adopted handle in place from the stored donor (adapter.fill for
resources, AssetRef._fill for values) instead of swapping to the
stored object, so per-catalog identity is preserved, and register
_handleKeys so release(handle) works.
… adoption

Flip Assets.from() leaves to meta-stamped handle-hybrids (Texture/Sound or
AssetRef) built via createLeaf, and route get/load of a catalog or a single
meta-stamped leaf through Loader._adopt (fill in place, scope-scoped claim).

- Assets.ts: leaves built with createLeaf; InferLeaf types resource kinds as
  the resource and value kinds as AssetRef via an explicit ValueAssetKind check
  (object-heuristic mis-types xml/srt/vtt/binary/wasm); export InferAssetsProperties.
- Loader.ts: _getClaimed + _loadClaimed adopt catalogs and single leaves;
  add _createAdoptedQueue reusing LoadingQueue progress/settle machinery;
  add get(catalog)/get(leaf)/load(leaf) overloads; rewrite unload(container)
  to release each leaf's claim; document _adopt's already-stored fast path (S7).

Breaking (pre-1.0): an already-constructed Asset in a catalog is now converted
to a leaf, not passed through by reference. Migrate shipped descriptor-contract
tests and add end-to-end catalog get/load tests.
SceneLoader.load already forwarded Assets<M> catalogs to _loadClaimed
untouched (added alongside the scene-scoped loader itself). SceneLoader.get
was missing the mirrored catalog/leaf overloads, so `scene.loader.get(catalog)`
type-checked against `never` and would not compile. Add the two overloads
(mirroring Loader.get(catalog)/Loader.get(leaf)) and widen the get()
implementation signature to accept `object`, forwarding arg0 unchanged to
_getClaimed — SceneLoader stays a pure scope-passing proxy.

Adds test/core/scene-loader-catalog.test.ts covering both get(catalog) and
load(catalog): leaves are claimed under the scene's own scope, and
scene.destroy() drops the last claim, evicting the payload back to 'loading'.
Regenerates site/src/content/api/{assets,loader}.json to reflect the S1
handle-hybrid leaf changes: Assets container description now documents
resource-leaf-is-the-resource / value-leaf-is-AssetRef semantics, and
Loader gains the get(catalog)/get(leaf)/load(leaf) overload docs.
…f parity

Loader.load's single generic leaf overload typed an AssetRef value leaf's
result as LoadingQueue<AssetRef<X>>, but AssetRef.loaded resolves to the raw
X at runtime. Add a discriminating load<T>(leaf: AssetRef<T>) overload ahead
of the generic one so it wins for value leaves.

SceneLoader.load was missing the single-leaf overloads that SceneLoader.get
already had, so scene.loader.load(assets.ship) typed against the wrong
greedy overload. Mirror Loader.load's leaf overloads (including the AssetRef
discriminator) verbatim, forwarding unchanged through _loadClaimed.

Also correct the _adopt §7 accepted-gap comment: it claimed the stale-fill
gap was unreachable in S1's usage surface, but it's reachable via a
duplicate source within a single catalog (second leaf hangs at 'loading').
Comment-only, no behavior change.
… hang (§7)

Loader._adopt's in-flight-not-yet-stored fall-through previously just
claimed a second, different handle for a key already registered under
_deferred or _refs, leaving it stuck at 'loading' forever with no
diagnostic (Assets.from({ a: 'x.png', b: 'x.png' }) then load(catalog)
hung silently). Now emits a logger.warn identifying the duplicate
source, while still claiming so refcounting stays correct. The
idempotent re-adopt of the SAME handle remains a silent no-op. Full
fix (per-key multi-handle tracking) is deferred to §7.
Close the asset-access design §7 duplicate-source hang: when two distinct
handles (or value refs) for one source are adopted while the fetch is still
in flight, both now heal from the single source-keyed decode instead of the
second silently hanging at 'loading'.

_deferred/_refs now track a Set of in-flight handles/refs per key (the
first-inserted member is the canonical representative that moves into
_resources on store, preserving the old single-handle eviction contract).
_adopt joins a distinct handle into the key's set; _storeResource,
_onTrackedFailure and the unload-in-flight path fill/fail every member.

Decouple the shared decode from per-handle sampler state: the texture
adapter applies each handle's own samplerOptions at createPlaceholder and
fill() now transplants ONLY the decoded source, so two handles for one
source keep independent samplers off one decode.

Re-scope the options-conflict warn to fire only on a genuine fetch-relevant
difference (e.g. mimeType) for the same source; differing samplerOptions /
pre-size are per-handle now and no longer warn. Removes the former
duplicate-source dev-warn (replaced by a real fill).

The later-evict co-handle orphan remains an accepted §7 remainder
(documented inline); no weak-retention this slice.
… placeholder

_getSeamless created a fresh placeholder from the raw get() options only,
never folding in the manifest/backgroundLoad-registered options for that
source the way _loadSingle resolves its fetch options (options ?? entry?.options).
A texture backgroundLoad()'d with samplerOptions and then fetched via a bare
get() silently rendered with the default sampler instead of the registered one.

Resolve the placeholder options identically to _loadSingle for a brand-new
deferred entry only; existing deferred entries keep reusing their stored
options unchanged.

Also harden the empty-handle-set path: if a live deferred entry's handle set
were ever empty, _getSeamless used to return undefined; now it falls through
and creates a fresh placeholder instead.
Introduce a uniform AssetStatus interface (state/ready/error) and the
_statusFields() helper that projects it from a handle's internal
LoadState, for later asset-handle/ref status-channel wiring.

- _statusFields is generic over LoadState's Owner type parameter:
  LoadState<Owner> is invariant in Owner (appears contravariantly in
  the settle/fail resolvers), so a fixed LoadState<unknown> parameter
  rejected every concrete LoadState<T> call site under strict TS.
- Re-export AssetStatus from the resources barrel (src/resources/index.ts),
  matching how other public types are exposed there.
- Add src/**/*.test.ts to the "exojs" vitest project's include globs so
  colocated tests (as specified for this task) are discovered alongside
  the existing test/**/*.test.ts suite.
The AssetStatus interface is now re-exported from src/resources/index.ts
(and therefore from the root src/index.ts barrel), so the committed
export-inventory snapshot needs the new "AssetStatus: interface" entry.
…tree

Drop the unused _statusFields helper (the per-handle state/ready/error
getters read LoadState inline) and revert the vitest colocated-test
include — asset-system tests live under test/resources/ per repo
convention.
…inference

Also fixes import-sort lint on extensionKindRegistry.test.ts from the prior commit.
Only texture/sound (seamless) and the 8 value kinds can build a placeholder
leaf via createLeaf; drop font/bmFont/svg suffix inference (loaded via X.of or
config, bare-path support is a follow-up). Also autofix import order in
asset-status.test.ts (pre-existing lint from the A2A3 task).
Exoridus added 3 commits July 9, 2026 17:31
Replace loader.get(Type, source)/loader.load(Type, source) with the
current API in package READMEs and site prose: bare-path load()/get()
for seamless types (Texture), and X.of(source, options) for
non-seamless/extension types (TiledMap, TileMap, AudioStream, Video).

loading-and-resources.mdx's "Manifests and bundles" section, its
"Custom asset types" section, and its "Legacy alias types still
throw" callout are left untouched — they document registerManifest/
loadBundle/hasBundle/defineAssetManifest/BundleLoadError/
registerAssetType/peek()/has(), all removed in 53f0613 (S3 P6a).
Fixing the token-form calls there in isolation would still leave
broken references to nonexistent exports; see
.superpowers/sdd/task-S3-docs-sweep-report.md for the follow-up this
needs.
…Map, expose SceneLoader background option, JSDoc

- Remove exojs-tiled's ExtensionTokenTypeMap augmentation (the interface was
  removed from the engine in S3; the block shipped a phantom removed-symbol type).
- Sweep 3 stale load(TiledMap, …) token-form JSDoc comments in exojs-tiled.
- SceneLoader.load mirrors Loader.load's LoadOptions (scene-scoped background).
- Note that get(X.of(src)) is not instance-deduped by source.
@Exoridus Exoridus changed the title feat(resources): asset-access redesign — catalog API, X.of(), status channel (S1+S2) feat(resources)!: asset-access redesign — catalog API, X.of(), defineAsset, status channel (S1+S2+S3) Jul 9, 2026
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bundle Report

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

Detailed changes
Bundle name Size Change
exo-esm-esm 1.63MB 10.64kB (0.66%) ⬆️
exo-esm-modules-esm 2.94MB 24.35kB (0.83%) ⬆️
exo-iife-min-Exo-iife 1.63MB 10.64kB (0.66%) ⬆️
exo-full-iife-Exo-iife 2.23MB 2.02kB (0.09%) ⬆️
exojs-aseprite-esm 27.63kB -350 bytes (-1.25%) ⬇️
exojs-audio-fx-esm 391.34kB -8 bytes (-0.0%) ⬇️
exojs-tiled-esm 122.41kB -443 bytes (-0.36%) ⬇️
exojs-particles-esm 203.7kB -10 bytes (-0.0%) ⬇️
exojs-ldtk-esm 55.66kB -65 bytes (-0.12%) ⬇️
exojs-tilemap-esm 252.63kB 28 bytes (0.01%) ⬆️
site-server-esm 14.39MB 101 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: site-server-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
_astro/ExoHeader.3RyRLrs3.js (New) 25.61kB 25.61kB 100.0% 🚀
_astro/ExoHeader.BN3N3fre.js (Deleted) -25.5kB 0 bytes -100.0% 🗑️
view changes for bundle: exojs-tilemap-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
tilemapSerializers.js 14 bytes 1.84kB 0.77%
tilemapSerializers.d.ts 14 bytes 930 bytes 1.53%
view changes for bundle: exo-iife-min-Exo-iife

Assets Changed:

Asset Name Size Change Total Size Change (%)
exo.iife.min.js -549 bytes 706.18kB -0.08%
resources/Loader.d.ts -3.96kB 35.1kB -10.14%
core/Scene.d.ts -1.93kB 12.33kB -13.51%
audio/Sound.d.ts 361 bytes 10.66kB 3.5%
resources/AssetDefinitions.d.ts 4.15kB 7.67kB 118.06% ⚠️
extensions/Extension.d.ts 509 bytes 7.63kB 7.14% ⚠️
rendering/texture/Texture.d.ts 367 bytes 5.55kB 7.08% ⚠️
resources/Assets.d.ts 3.14kB 4.51kB 230.11% ⚠️
rendering/text/HTMLText.d.ts 18 bytes 3.69kB 0.49%
resources/index.d.ts -2 bytes 3.61kB -0.06%
resources/defineAsset.d.ts (New) 2.87kB 2.87kB 100.0% 🚀
resources/seamless.d.ts 634 bytes 2.83kB 28.82% ⚠️
core/LoadState.d.ts 536 bytes 2.73kB 24.44% ⚠️
resources/tokens.d.ts 502 bytes 2.67kB 23.11% ⚠️
resources/Asset.d.ts 1.55kB 2.45kB 173.36% ⚠️
resources/AssetRef.d.ts 807 bytes 2.11kB 61.93% ⚠️
rendering/text/BmFont.d.ts 20 bytes 1.82kB 1.11%
resources/factories/TextureFactory.d.ts 21 bytes 1.7kB 1.25%
resources/extensionKindRegistry.d.ts (New) 1.41kB 1.41kB 100.0% 🚀
resources/assetMeta.d.ts (New) 936 bytes 936 bytes 100.0% 🚀
resources/assetKindRegistry.d.ts (New) 924 bytes 924 bytes 100.0% 🚀
resources/AssetStatus.d.ts (New) 836 bytes 836 bytes 100.0% 🚀
resources/AssetManifest.d.ts (Deleted) -2.53kB 0 bytes -100.0% 🗑️
view changes for bundle: exojs-ldtk-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
loadLdtkMap.js 59 bytes 6.63kB 0.9%
ldtkBinding.js 51 bytes 986 bytes 5.45% ⚠️
ldtkBinding.d.ts -175 bytes 667 bytes -20.78%
view changes for bundle: exojs-tiled-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
loadTiledMap.js 40 bytes 4.26kB 0.95%
public.d.ts -175 bytes 2.78kB -5.92%
tiledRuntimeMapBinding.js 58 bytes 1.66kB 3.63%
tiledExtension.js 28 bytes 1.41kB 2.02%
tiledOptions.d.ts 14 bytes 1.33kB 1.06%
tiledRuntimeMapBinding.d.ts -244 bytes 1.11kB -17.99%
tiledMapBinding.js 53 bytes 1.06kB 5.27% ⚠️
tiledExtension.d.ts 28 bytes 813 bytes 3.57%
tiledMapBinding.d.ts -245 bytes 601 bytes -28.96%
view changes for bundle: exojs-particles-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
ParticleSystem.js -5 bytes 21.5kB -0.02%
ParticleSystem.d.ts -5 bytes 9.71kB -0.05%
view changes for bundle: exojs-audio-fx-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
effects/ConvolutionEffect.js -4 bytes 7.31kB -0.05%
effects/ConvolutionEffect.d.ts -4 bytes 4.53kB -0.09%
view changes for bundle: exo-full-iife-Exo-iife

Assets Changed:

Asset Name Size Change Total Size Change (%)
exo.full.iife.js 2.02kB 2.23MB 0.09%
view changes for bundle: exo-esm-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
exo.esm.js -550 bytes 699.64kB -0.08%
resources/Loader.d.ts -3.96kB 35.1kB -10.14%
core/Scene.d.ts -1.93kB 12.33kB -13.51%
audio/Sound.d.ts 361 bytes 10.66kB 3.5%
resources/AssetDefinitions.d.ts 4.15kB 7.67kB 118.06% ⚠️
extensions/Extension.d.ts 509 bytes 7.63kB 7.14% ⚠️
rendering/texture/Texture.d.ts 367 bytes 5.55kB 7.08% ⚠️
resources/Assets.d.ts 3.14kB 4.51kB 230.11% ⚠️
rendering/text/HTMLText.d.ts 18 bytes 3.69kB 0.49%
resources/index.d.ts -2 bytes 3.61kB -0.06%
resources/defineAsset.d.ts (New) 2.87kB 2.87kB 100.0% 🚀
resources/seamless.d.ts 634 bytes 2.83kB 28.82% ⚠️
core/LoadState.d.ts 536 bytes 2.73kB 24.44% ⚠️
resources/tokens.d.ts 502 bytes 2.67kB 23.11% ⚠️
resources/Asset.d.ts 1.55kB 2.45kB 173.36% ⚠️
resources/AssetRef.d.ts 807 bytes 2.11kB 61.93% ⚠️
rendering/text/BmFont.d.ts 20 bytes 1.82kB 1.11%
resources/factories/TextureFactory.d.ts 21 bytes 1.7kB 1.25%
resources/extensionKindRegistry.d.ts (New) 1.41kB 1.41kB 100.0% 🚀
resources/assetMeta.d.ts (New) 936 bytes 936 bytes 100.0% 🚀
resources/assetKindRegistry.d.ts (New) 924 bytes 924 bytes 100.0% 🚀
resources/AssetStatus.d.ts (New) 836 bytes 836 bytes 100.0% 🚀
resources/AssetManifest.d.ts (Deleted) -2.53kB 0 bytes -100.0% 🗑️
view changes for bundle: exo-esm-modules-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
resources/Loader.js 1.79kB 78.89kB 2.32%
resources/Loader.d.ts -3.96kB 35.1kB -10.14%
audio/Sound.js 469 bytes 20.16kB 2.38%
core/Scene.js -157 bytes 14.28kB -1.09%
core/Scene.d.ts -1.93kB 12.33kB -13.51%
index.js -27 bytes 11.0kB -0.24%
audio/Sound.d.ts 361 bytes 10.66kB 3.5%
rendering/text/HTMLText.js 18 bytes 10.44kB 0.17%
resources/coreAssetBindings.js 1.7kB 9.53kB 21.75% ⚠️
rendering/texture/Texture.js 475 bytes 8.98kB 5.59% ⚠️
resources/AssetDefinitions.d.ts 4.15kB 7.67kB 118.06% ⚠️
extensions/Extension.d.ts 509 bytes 7.63kB 7.14% ⚠️
core/serialization/serialize.js 9 bytes 6.46kB 0.14%
rendering/texture/Texture.d.ts 367 bytes 5.55kB 7.08% ⚠️
resources/Assets.d.ts 3.14kB 4.51kB 230.11% ⚠️
core/LoadState.js 689 bytes 3.92kB 21.35% ⚠️
resources/factories/BmFontFactory.js -13 bytes 3.76kB -0.34%
rendering/text/HTMLText.d.ts 18 bytes 3.69kB 0.49%
resources/seamless.js 275 bytes 3.65kB 8.14% ⚠️
resources/index.d.ts -2 bytes 3.61kB -0.06%
resources/Assets.js 2.37kB 3.48kB 212.29% ⚠️
resources/AssetRef.js 1.15kB 3.15kB 57.47% ⚠️
resources/defineAsset.d.ts (New) 2.87kB 2.87kB 100.0% 🚀
resources/seamless.d.ts 634 bytes 2.83kB 28.82% ⚠️
resources/tokens.js 760 bytes 2.81kB 37.04% ⚠️
core/LoadState.d.ts 536 bytes 2.73kB 24.44% ⚠️
resources/tokens.d.ts 502 bytes 2.67kB 23.11% ⚠️
resources/Asset.d.ts 1.55kB 2.45kB 173.36% ⚠️
resources/AssetRef.d.ts 807 bytes 2.11kB 61.93% ⚠️
resources/defineAsset.js (New) 1.98kB 1.98kB 100.0% 🚀
resources/extensionKindRegistry.js (New) 1.97kB 1.97kB 100.0% 🚀
resources/assetKindRegistry.js (New) 1.95kB 1.95kB 100.0% 🚀
rendering/text/BmFont.d.ts 20 bytes 1.82kB 1.11%
resources/factories/TextureFactory.d.ts 21 bytes 1.7kB 1.25%
resources/extensionKindRegistry.d.ts (New) 1.41kB 1.41kB 100.0% 🚀
resources/Asset.js 436 bytes 962 bytes 82.89% ⚠️
resources/assetMeta.d.ts (New) 936 bytes 936 bytes 100.0% 🚀
resources/assetKindRegistry.d.ts (New) 924 bytes 924 bytes 100.0% 🚀
resources/assetMeta.js (New) 877 bytes 877 bytes 100.0% 🚀
rendering/text/BmFont.js 20 bytes 863 bytes 2.37%
resources/AssetStatus.d.ts (New) 836 bytes 836 bytes 100.0% 🚀
resources/AssetManifest.js (Deleted) -3.58kB 0 bytes -100.0% 🗑️
resources/AssetManifest.d.ts (Deleted) -2.53kB 0 bytes -100.0% 🗑️
view changes for bundle: exojs-aseprite-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
AsepriteSheet.js 14 bytes 10.37kB 0.14%
asepriteBinding.js -265 bytes 4.79kB -5.25%
AsepriteSheet.d.ts 14 bytes 4.49kB 0.31%
asepriteBinding.d.ts -141 bytes 948 bytes -12.95%
asepriteExtension.js 14 bytes 878 bytes 1.62%
asepriteExtension.d.ts 14 bytes 589 bytes 2.43%

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Patch % Lines
packages/exojs-ldtk/src/LdtkMap.ts 0.00% 1 Missing ⚠️
packages/exojs-particles/src/ParticleSystem.ts 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Exoridus added 16 commits July 9, 2026 23:38
… B1)

Clean pre-1.0 break: the public asset-config discriminator is now `kind`
(matching `defineAsset.kind`); the legacy `{ type, source }` shape is a type
error, no compatibility shim. Renames AnyAssetConfig, the Asset.kind getter,
InferAssetResource/InferCatalogLeaf, the Assets normalize/leaf path, the
loader dispatch + handler-config strip, and all in-repo test/package configs.
Guides + examples migrate in a later slice.
…lta C1)

A catalog leaf built by createLeaf now starts 'idle' (not 'loading') until a
loader adopts it; _adopt transitions idle->loading, preserving any pending
.loaded promise so an awaiter taken before adoption still settles. Manually
constructed resources stay 'ready'. Existing pre-adoption assertions updated
from 'loading' to 'idle'.
The background-drain verb is renamed to state its exact meaning (wait until
background work is done). Updates the method, its JSDoc references, callers,
and the regenerated API docs. Guide prose migrates in the examples/guides slice.
Builds one meta-stamped idle leaf from any single descriptor (bare path,
X.of()/Asset.kind() descriptor, or explicit config) — the explicit
single-asset alternative to a one-field catalog.
…lta G1)

Assets.group(kind, entries, shared?) builds a record of same-kind configs to
spread into Assets.from, merging shared options under per-entry overrides. The
nested-field form (textures: Assets.group(...)) is deferred — it would force
Assets.from to treat arbitrary config records as nested catalogs, weakening the
CatalogEntry constraint (design 07 §12 marks nested a nice-to-have).
… in a catalog (delta H1a)

Asset.kind<Config>('json', …) now returns a branded ValueAsset<Config>;
InferCatalogLeaf checks the brand BEFORE the T extends object heuristic, so an
object-valued JSON descriptor classifies as AssetRef<Config> (not Config) — the
delta §4 typed-JSON contract. Resource kinds stay direct resource leaves; the
unbranded legacy X.of() heuristic is preserved (additive). Type-only change.
A value config may carry parse: (raw) => T — applied per-leaf on AssetRef fill
(kept out of the source-keyed fetch opts). InferCatalogLeaf/InferAssetResource
derive the leaf/resolved type from parse's return, so { kind:'json', source,
parse } is AssetRef<T> in a catalog and T in the resolved map (delta §4/§5).
parse is typed value-kind-only.
Internal BmFont sub-texture loads and the incidental .of() usages in the
resources test suite now use Asset.kind('texture'|'json'|…, src). The .of()
statics and their dedicated tests (of-statics, of-descriptor-options) stay
until the deletion slice.
Sweeps X.of(src) -> Asset.kind('<kind>', src) across the example catalog using
each static's real kind (AudioStream->music, SubtitleAsset->vtt, …), adds the
Asset import, and regenerates the committed .js via examples:sync. typecheck:
examples green; full smoke runs in the review slice (needs site:build).
…delta H4)

Guide code blocks + prose move to Asset.kind / { kind, source } / awaitBackground
(the custom-kind HeightField tutorial too); typecheck:guides green. Regenerates
the API docs for the Asset.kind JSDoc, the new ValueAsset export, and parse.
# Conflicts:
#	examples/application-scenes/camera-basic.ts
#	examples/application-scenes/multi-view-split-screen.ts
#	examples/application-scenes/pause-and-resume.ts
#	examples/application-scenes/picture-in-picture.ts
#	examples/audio-basics/audio-buses.ts
#	examples/audio-basics/crossfade-tracks.ts
#	examples/audio-basics/music-loop.ts
#	examples/audio-basics/play-sound.ts
#	examples/audio-basics/random-pitch-pool.ts
#	examples/audio-basics/sound-pool.ts
#	examples/audio-fx/compressor.ts
#	examples/audio-fx/ducking.ts
#	examples/audio-fx/reverb-and-delay.ts
#	examples/audio-fx/vocoder.ts
#	examples/beat-detection/beat-sync-pulse.ts
#	examples/beat-detection/frequency-bands.ts
#	examples/beat-detection/tempo-tracking.ts
#	examples/custom-renderers/custom-render-pass.ts
#	examples/debug-layer/bounding-boxes.ts
#	examples/debug-layer/performance-overlay.ts
#	examples/debug-layer/pointer-and-hittest.ts
#	examples/filters/blur-filter.ts
#	examples/filters/chromatic-aberration.ts
#	examples/filters/color-filter.ts
#	examples/filters/crt-scanlines.ts
#	examples/filters/custom-fragment-shader.ts
#	examples/filters/filter-stack.ts
#	examples/filters/noise-vignette.ts
#	examples/filters/palette-cycling.ts
#	examples/geometry-graphics/infinite-grid.ts
#	examples/geometry-graphics/mesh-deformed-grid.ts
#	examples/geometry-graphics/mesh-textured-quad.ts
#	examples/getting-started/game-loop.ts
#	examples/getting-started/hello-world.ts
#	examples/getting-started/resize-and-dpr.ts
#	examples/input/action-mapping.ts
#	examples/input/gamepad.ts
#	examples/input/mouse-and-pointer.ts
#	examples/input/multi-gamepad.ts
#	examples/particles/bonfire.ts
#	examples/particles/cursor-attractor-particles.ts
#	examples/particles/custom-wgsl-module.ts
#	examples/particles/emitter-basics.ts
#	examples/particles/fireworks.ts
#	examples/particles/gpu-particles.ts
#	examples/performance/backend-comparison.ts
#	examples/physics/sprite-follows-body.ts
#	examples/physics/tiled-map-physics-actor.ts
#	examples/render-targets/bloom-lite.ts
#	examples/render-targets/render-to-texture.ts
#	examples/render-targets/trail-feedback.ts
#	examples/render-targets/water-mirror.ts
#	examples/scene-graph/containers.ts
#	examples/scene-graph/local-vs-global-transform.ts
#	examples/scene-graph/masks.ts
#	examples/scene-graph/pivot-and-anchor.ts
#	examples/scene-graph/z-ordering.ts
#	examples/showcase/audio-reactive-particles.ts
#	examples/showcase/audio-visualisation.ts
#	examples/showcase/boss-intro-cinematic.ts
#	examples/showcase/color-grading.ts
#	examples/showcase/damage-flash.ts
#	examples/showcase/dialog-system.ts
#	examples/showcase/gamepad-spaceship.ts
#	examples/showcase/loading-progress-with-shader.ts
#	examples/showcase/low-band-camera-shake.ts
#	examples/showcase/pause-blur.ts
#	examples/showcase/rectangles-collision.ts
#	examples/showcase/screen-shake-on-explosion.ts
#	examples/showcase/typewriter-text.ts
#	examples/showcase/vinyl-record.ts
#	examples/spatial-audio/falloff-curves.ts
#	examples/spatial-audio/listener-and-source.ts
#	examples/spatial-audio/moving-source.ts
#	examples/sprites-textures/blendmodes.ts
#	examples/sprites-textures/sprite-basics.ts
#	examples/sprites-textures/spritesheet-frames.ts
#	examples/sprites-textures/svg-drawable.ts
#	examples/sprites-textures/texture-loader.ts
#	examples/sprites-textures/video-drawable.ts
#	examples/text-fonts/basic-text.ts
#	examples/text-fonts/bitmap-text-basic.ts
#	examples/text-fonts/web-fonts.ts
#	examples/tweens-animation/frame-animation.ts
#	examples/tweens-animation/interrupt-and-replace.ts
#	examples/tweens-animation/tween-basics.ts
#	examples/tweens-animation/tween-chains.ts
#	examples/tweens-animation/tween-from-array.ts
#	examples/tweens-animation/tween-with-yoyo.ts
#	site/src/content/api/loader.json
#	src/core/Scene.ts
#	src/resources/Loader.ts
#	test/resources/loader-seamless.test.ts
…lta I1)

Removes the .of() annotation statics from every core resource/token class and
the extension packages (Texture/Sound/…/TileMap/AsepriteSheet/LdtkMap); Asset.kind
is now the sole descriptor builder. _makeAsset folds into Asset.kind (export
dropped). Migrates the remaining internal + package/test .of call sites and JSDoc
to Asset.kind, deletes the .of-specific test files (of-statics, of-descriptor-
options, {aseprite,ldtk,tiled,tilemap}-of), and adds a SceneLoader.get(asset)
overload mirroring Loader.get. Also switches the one LDtk fetchJson cast to the
generic fetchJson<T>() form. Token classes stay as internal kind tags.
… typeNames (delta I2)

Drops the load<M>(config: M) overload from Loader + SceneLoader (catalogs go
through Assets.from); a bare record no longer resolves as a catalog. Marks the
defineAsset typeNames field @internal (kind is the public extension key).
Updates the last X.of() JSDoc reference to Asset.kind.
@Exoridus Exoridus changed the title feat(resources)!: asset-access redesign — catalog API, X.of(), defineAsset, status channel (S1+S2+S3) feat(resources)!: asset-access redesign — Asset.kind, kind discriminator, idle/awaitBackground, Assets.one/group + lifecycle merge Jul 10, 2026

@Exoridus Exoridus left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: asset-access finalization delta

Verdict: APPROVE WITH FIXUPS — architecture is sound and the delta is substantially implemented, but I would not merge before the three fixups below.

I reviewed the current PR head 157e5da4.

What looks good

  • The PR body now accurately frames this as the final post-S3 delta and documents the intended public API: Asset.kind, { kind, source }, Assets.one, Assets.group, idle, awaitBackground, and lifecycle integration.
  • Asset.kind(...) is strongly typed in the main happy paths: kind autocomplete, resource inference, value-kind generic, and negative tests for resource-kind generics are present.
  • The value-brand fix is the right solution for Asset.kind<Config>('json', ...) inside catalogs: catalog fields become AssetRef<Config>, while loader.load(catalog) resolves to Config.
  • idle is implemented at the right layer: createLeaf(...) marks unadopted resource/value leaves idle, and _adopt(...) transitions idle leaves to loading.
  • parse now exists as both type/runtime behavior for object-form value specs.
  • awaitBackground() naming and semantics are correct.
  • Assets.one(...) and spread-form Assets.group(...) are good additions and keep the core API small.

Required fixups before merge

1. loader.get(Asset.kind<Config>('json', ...)) is still type-unsound

The value-brand fix currently solves catalog inference, but not direct get(asset).

Current public overload:

public get<T>(asset: Asset<T>): T extends object ? T : AssetRef<T>;

That means:

const cfg = loader.get(Asset.kind<Config>('json', 'config.json'));

is typed as Config, while runtime returns an AssetRef<Config>. The JSDoc even still documents this mismatch. This is exactly the object-valued JSON problem we fixed for catalogs, but it remains in get(asset) and is mirrored in SceneLoader.get(asset).

Please change the overloads to use the brand instead of the old T extends object heuristic, e.g. roughly:

public get<T>(asset: ValueAsset<T>): AssetRef<T>;
public get<T>(asset: Asset<T>): T;

and mirror that in SceneLoader.

Add a type-test for:

loader.get(Asset.kind<Config>('json', 'config.json')) // AssetRef<Config>
loader.get(Asset.kind('texture', 'player.png'))       // Texture

2. Public JSDoc/API docs still reference the removed .of() / { type } surface

There are several stale public comments that will leak into generated API docs and contradict the clean break:

  • Assets.from(...) docs still say catalog entries may be an X.of() descriptor and unregistered suffixes need X.of().
  • Assets.one(...) docs still say it accepts an X.of() descriptor.
  • Loader.load(...) docs still mention Asset<T> from X.of(...) and inline config maps with { type, source }.
  • Loader.load(path) docs still say use load(MyType.of(path)).
  • Loader.get(asset) docs still talk about X.of, Texture.of, Json.of, and load(X.of(...)).
  • defineAsset(...) docs still say non-leaf resource kinds must be declared via X.of().
  • tokens.ts has an old comment explaining token behavior in terms of X.of().
  • tiledRuntimeMapBinding.ts still describes loader.load(TileMap, 'world.tmj') / load(TiledMap) token-style flows.

This is mostly textual, but it is a public API consistency issue. Replace those references with Asset.kind(...), { kind, source }, Assets.one(...), or mark the relevant token lookup as legacy/advanced where it genuinely still exists.

3. Inline record-catalog runtime branch still exists and is documented in Loader._loadClaimed

The PR body says loader.load({ a, b }) inline record-catalog was removed. The TypeScript overload is gone, but the implementation still has a plain config-map fallback branch:

// 3. Plain config map: Record<string, AssetInput>
const configMap = arg0 as Record<string, AssetInput>;
...

For JS consumers this is still a runtime surface, and for invalid old calls it may fail later/oddly rather than with a clean migration error.

Please either:

  • remove this branch and throw a direct migration error for plain objects: Use Assets.from({...}), or
  • explicitly keep it as a documented legacy/advanced runtime path and update the PR body accordingly.

Given the clean-break direction, I recommend removing it or turning it into an explicit error.


Secondary fixups / follow-ups

4. Explicit value-kind generics may lose kind-specific options

Current overloads are:

kind<K extends keyof AssetDefinitions>(kind: K, source: string, options?: OptionsForKind<K>): ...
kind<T>(kind: ValueAssetKind, source: string, options?: OptionsForKind<ValueAssetKind>): ValueAsset<T>

The second overload uses OptionsForKind<ValueAssetKind>, which may not preserve kind-specific options for explicit generic value calls like typed CSV with { delimiter }.

Please add a type-test for something like:

Asset.kind<string[][]>('csv', 'table.csv', { delimiter: ';' })

If that fails, either fix the overload or document that typed value-kinds with kind-specific options should use object form + parse.

5. Decide whether parse is sync-only

Runtime currently treats parse synchronously on _fill. That is fine, but then docs/spec should say sync-only. If async parse is intended, _fill/store/fill flow needs different handling.

6. loadContainer(...) and token classes need clearer status

loadContainer(...) remains a legacy/experimental container path and token classes remain exported for dispatch/internal compatibility. That is acceptable, but the public docs should make that status explicit so it doesn't read like the old token API survived as a primary surface.


Final assessment

The implementation is close. I would merge after:

  1. fixing get(Asset.kind<Config>('json')) return typing via ValueAsset overloads,
  2. cleaning stale .of() / { type } / token-form JSDoc,
  3. removing or explicitly erroring the runtime inline-record branch.

Everything else can be follow-up if documented.

…dle (delta J review)

Reconciles the PR review + the workflow code-review (both flagged the same #1):
- get(asset)/SceneLoader.get(asset) now use the ValueAsset brand instead of the
  T extends object heuristic, so get(Asset.kind<Config>('json')) is AssetRef<Config>
  at type-level (matching runtime) — for ALL object-valued kinds (xml/vtt/binary/
  wasm/typed-json), not just json. Adds type-tests.
- AssetRef._fill wraps parse: a throwing parse fails only its own ref, never a
  sibling ref already 'ready' on the same shared source. Adds a runtime test.
- State-getter JSDoc (Texture/Sound/AssetRef/AssetStatus) + LoadStateValue now
  document 'idle'.
- Swept remaining stale X.of()/{ type }/token-form JSDoc to Asset.kind/{ kind }.
- Inline record-catalog: typed overload stays removed; the runtime path is kept +
  documented as internal/legacy (multi-alias/identity plumbing + coverage).

Deferred (pre-existing S1-S3 gaps, not this delta): failed-shared-fetch join
hang, failed-catalog-leaf reget no-op, createLeaf error-message specificity.
@Exoridus

Copy link
Copy Markdown
Owner Author

Review reconciliation — addressed in bf3f1b7c

Cross-checked this review against a parallel high-effort automated code-review; both independently flagged the same top issue (get-brand). Fixed:

  1. get(Asset.kind<Config>('json')) type-unsoundness (your Replace Transformable base class with property #1 = the automated review's Replace Transformable base class with property #1). Loader.get(asset) and SceneLoader.get(asset) now use the ValueAsset brand (get<T>(asset: ValueAsset<T>): AssetRef<T> / get<T>(asset: Asset<T>): T) instead of the T extends object heuristic. This was broader than just JSON — it affected every object-valued kind (xml/vtt/binary/wasm). Added type-tests for value→AssetRef and resource→resource.
  2. Stale .of() / { type } / token-form JSDoc (your Primitive Shapes #2). Swept across Assets.from/Assets.one/Loader.load/Loader.get/defineAsset/tokens/tiled bindings → Asset.kind(...) / { kind, source }. Also documented idle on the state getters (a drift the automated review caught). API docs regenerated + in sync.
  3. Inline record-catalog (your Masking #3). The typed overload stays removed (typed catalogs go through Assets.from); the runtime path is kept and documented as internal/legacy — ~10 loader-semantics tests (multi-alias dedup, unload-by-identity) exercise it, and per your "or document that status" option this is the lower-churn choice. PR body updated to reflect this precisely.

Also fixed one issue the automated review surfaced that this delta introduced: a throwing parse in AssetRef._fill now fails only its own ref, never a sibling ref already 'ready' on the same shared source (runtime test added).

Deferred (pre-existing S1–S3 gaps, not introduced by this delta): failed-shared-fetch join hang, failed-catalog-leaf reget no-op (both in the _adopt/_refs failure paths documented in the loader spec §7/gaps), and the swallowed createLeaf error-message specificity. Tracked for a follow-up loader-hardening slice rather than folded into this API delta.

All gates green: verify:quick + full suite (7025) + zero-hit clean-break grep.

- Loader.get(asset) JSDoc: drop the stale Texture.of/Json.of/TextAsset.of refs
  and the obsolete 'T extends object heuristic can't distinguish' remark (the
  ValueAsset brand solved exactly that).
- createLeaf error message: use load(Asset.kind('<kind>', ...)) instead of the
  removed load(...of(...)).
- Loader.load JSDoc: drop 'Config map' as a public form; note catalogs go through
  Assets.from and the record path is an internal runtime fallback only.
- get(type, alias): documented as an @advanced legacy in-memory lookup (a cache
  lookup, not the removed token fetch), in Loader + SceneLoader.
- parse documented as SYNCHRONOUS-only (config type + AssetRef); async is a
  follow-up.
@Exoridus Exoridus merged commit 0ad3c36 into main Jul 10, 2026
16 checks passed
@Exoridus Exoridus deleted the feat/v0.16-loader-access-redesign branch July 10, 2026 01:51
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