feat(resources)!: asset-access redesign — Asset.kind, kind discriminator, idle/awaitBackground, Assets.one/group + lifecycle merge#267
Conversation
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.
… Scene lifecycle hooks
…es AssetRef<unknown> mis-resolution
…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.
…AssetRef, Texture, Sound (§6)
…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).
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.
Bundle ReportChanges will increase total bundle size by 46.9kB (0.19%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: site-server-esmAssets Changed:
view changes for bundle: exojs-tilemap-esmAssets Changed:
view changes for bundle: exo-iife-min-Exo-iifeAssets Changed:
view changes for bundle: exojs-ldtk-esmAssets Changed:
view changes for bundle: exojs-tiled-esmAssets Changed:
view changes for bundle: exojs-particles-esmAssets Changed:
view changes for bundle: exojs-audio-fx-esmAssets Changed:
view changes for bundle: exo-full-iife-Exo-iifeAssets Changed:
view changes for bundle: exo-esm-esmAssets Changed:
view changes for bundle: exo-esm-modules-esmAssets Changed:
view changes for bundle: exojs-aseprite-esmAssets Changed:
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
… 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
left a comment
There was a problem hiding this comment.
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 becomeAssetRef<Config>, whileloader.load(catalog)resolves toConfig. idleis implemented at the right layer:createLeaf(...)marks unadopted resource/value leaves idle, and_adopt(...)transitions idle leaves to loading.parsenow exists as both type/runtime behavior for object-form value specs.awaitBackground()naming and semantics are correct.Assets.one(...)and spread-formAssets.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')) // Texture2. 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 anX.of()descriptor and unregistered suffixes needX.of().Assets.one(...)docs still say it accepts anX.of()descriptor.Loader.load(...)docs still mentionAsset<T>fromX.of(...)and inline config maps with{ type, source }.Loader.load(path)docs still say useload(MyType.of(path)).Loader.get(asset)docs still talk aboutX.of,Texture.of,Json.of, andload(X.of(...)).defineAsset(...)docs still say non-leaf resource kinds must be declared viaX.of().tokens.tshas an old comment explaining token behavior in terms ofX.of().tiledRuntimeMapBinding.tsstill describesloader.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:
- fixing
get(Asset.kind<Config>('json'))return typing viaValueAssetoverloads, - cleaning stale
.of()/{ type }/ token-form JSDoc, - 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.
Review reconciliation — addressed in
|
- 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.
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
What changed (finalization delta, per-slice)
Asset.kind()— the single typed descriptor builder (kind autocomplete fromAssetDefinitions, resource inference from kind, kind-specific options,<T>only for value kinds).type→kind(pre-1.0 clean break — no shim).idleload state for unadopted catalog leaves (usable placeholder; not for manually-constructed resources).load(catalog)returns a resolved value map (refs unwrapped).loadAll()→awaitBackground().Assets.one()andAssets.group()(spread form).Asset.kind<Config>('json', …)classifies asAssetRef<Config>in a catalog (object-valued JSON no longer misread as a resource).parsepost-load transform (type + runtime; per-leaf, applied on fill).feat/v0.16-loader-lifecycle— examples move offScene.load()/unload()hooks toinit()while keeping the final asset API; adds aSceneLoader.get(asset)overload mirroringLoader.get..of()statics everywhere (core + token + extension classes), internalized_makeAsset, removed the inline record-catalogloadoverload, and markedtypeNames@internal.Asset.kindis the sole descriptor builder.Breaking changes (pre-1.0)
{ type, source }→{ kind, source }.X.of(src)→Asset.kind('<kind>', src)(e.g.AudioStream.of→Asset.kind('music', …)).loader.load(Type, src)/loader.get(Type, src)fetch-by-token forms removed. A legacy in-memoryget(type, alias)lookup (a cache read, not a fetch) remains for non-seamless /loadContainerassets — marked@advanced.load({ a, b })overload is removed — build catalogs withAssets.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:quickgreen (typecheck + guides + examples + type-tests + packages + lint:all + format:check + docs:api:check)..of/loadAll/{ type }/ token forms remain).Deferred / notes
Assets.groupnested-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.site:build) not run here: it needs the built enginedist+ vendor sync (environmental); example correctness is covered bytypecheck:examples/typecheck:guides.parseis synchronous-only in v1 (maps the decoded raw value; it may not return aPromise). Asyncparseis a follow-up — it would need the fill/store flow to await.get()reget no-op vs the documented retry, and swallowedcreateLeaferror specificity.