refactor(resources): extract AssetResidency from Loader (Loader split, Slice 3/4)#416
Merged
Conversation
Exoridus
enabled auto-merge (squash)
July 26, 2026 05:56
added 5 commits
July 26, 2026 08:00
Standalone class owning claim/refcount tracking, in-flight fetch dedup, the resident-resource store, deferred-handle/value-ref healing (asset-system v2 section 7), and the background-loading queue — everything left in Loader that isn't type identification (Slice 1) or raw fetch/decode (Slice 2). Dispatches onProgress/onLoaded/onError through Loader's own Signal instances, passed in at construction (AssetResidencySignals) — mirrors Slice 2's storeResource-callback pattern, simpler here since Signal already has the right shape. Not yet wired into Loader — this commit is additive only, proven correct in isolation. Loader.ts's own copy of this logic still exists unchanged. Also adds a narrow eslint.config.ts override for the new file (complexity, no-unsafe-assignment) mirroring Loader.ts's existing exemption for this exact, relocated-verbatim branchy logic.
Six Important findings from task review:
1. destroy() clears _claims/_evicted, a deliberate deviation from the
original Loader.destroy() (ruled: keep it, a full teardown SHOULD
clear claim/eviction bookkeeping too) — now documented inline as
intentional rather than silent.
2. Test file reformatted with prettier (format:check is a separate
gate from eslint and wasn't run in the first pass).
3. 'claim on an evicted key re-drives the fetch and heals the same
handle' now snapshots the request count immediately before the
re-claim and asserts an increase, plus asserts adapter.fill and
_peekResource on the SAME handle object — the old assertion
(requests.length > 0) was already true from the first _getSeamless
call and would have passed even with the re-fetch branch deleted.
4. 'setConcurrency changes how many entries drain concurrently' now
actually exercises drain behavior at two concurrency levels instead
of asserting `residency` is truthy.
5. Added coverage of the AssetResidencySignals dispatch boundary — the
one piece of this file that isn't a verbatim relocation: onLoaded
assertion in the multi-handle-fill test, a new onError test for a
rejected fetch, and an onProgress assertion in the background-queue
test.
6. Restored ~60 lines of correctness-rationale comments that existed
in the original Loader.ts (still present there pre-Task-2) but were
dropped during the verbatim relocation: the begin()-before-fill
'failed' handle rationale, the §4.7 free-on-arrival explanation, the
in-flight identity-guard rationale in _trackInFlight, the audit-A5
fallback in _evictKey, the unloadOne snapshot-before-delete note,
per-branch _adopt rationale, the _areOptionsEquivalent exotic-type
note, and the unloadAll settled-vs-live paragraph.
Also both optional minors: swapped the unresolvable {@link Loader} to
plain backtick-code, and replaced the test setup's `as never` signals
cast with `as unknown as AssetResidencySignals`.
…lice 3b) Loader composes one AssetResidency instance and delegates claim/ release/releaseScope/adopt/peekResource/keyFor/unloadAll/ awaitBackground/setConcurrency to it as thin same-named wrappers, preserving the exact @internal entry-point contract SceneLoader.ts and serialize.ts depend on. load()/get()/unload()'s call-shape dispatch methods (_loadClaimed/_getClaimed/unload) keep their branching bodies on Loader, with the handful of lines that touched now-moved state (loadSingle/loadSingleAsset/getSeamless/getRef/the legacy-token resource lookup/the identity-alias lookup/the handle-key lookup) redirected through this._residency. No public API change. Loader.ts drops the bulk of its remaining private surface; the extracted logic is unchanged, only relocated and proven independently in AssetResidency's own test file (prior commit). Updates the handful of white-box test casts that reached directly into fields moved onto AssetResidency (_claims/_deferred/_resources/ _backgroundQueue/_evicted/_isQueuedInBackground) to go one level deeper through loader['_residency'], mirroring how Slice 1's Task 2 handled its _key white-box casts. Claude-Session: https://claude.ai/code/session_01VBChHCH6BZqQsLNfpGAXfa
…d nullish resource _getClaimed's legacy-token branch (Loader split, Slice 3b) reused AssetResidency._peekResource for its "is this loaded?" throw check, but _peekResource collapses "no entry" and "entry present with a null/undefined value" to the same null return via `?? null`. A bindAsset-bound custom type whose handler legitimately resolves to null/undefined (the branch's actual use case — loadContainer-loaded assets) would incorrectly throw "Missing resource" on a subsequent get(), even though the resource was genuinely stored. Adds AssetResidency._hasStored(type, source), a Map.has()-based presence check that distinguishes those two cases, and gates the throw on it instead of on the peeked value. _peekResource itself is unchanged and still backs the actual return value. Covering test: loader-seamless.test.ts's new "legacy alias lookup round-trips a bindAsset-bound type whose handler legitimately resolves to null/undefined" case, which fails against the prior _peekResource-gated check and passes with _hasStored. Claude-Session: https://claude.ai/code/session_01VBChHCH6BZqQsLNfpGAXfa
_getClaimed's legacy-token branch returned its resource through _peekResource, which coalesces a stored undefined to null. That accessor's null-collapsing contract predates this slice and is correct for its other callers, but this branch never routed through it before the residency split (it read the Map directly). Add AssetResidency._getStored, a raw (non-coalescing) lookup, and use it here so a bindAsset-bound type whose handler legitimately resolves undefined still round-trips through loader.get() as undefined instead of null.
Exoridus
force-pushed
the
worktree-loader-split-slice1
branch
from
July 26, 2026 06:04
76bcf62 to
2e19490
Compare
Bundle ReportChanges will increase total bundle size by 16.66kB (0.07%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: exo-iife-min-Exo-iifeAssets Changed:
Files in
view changes for bundle: exo-esm-esmAssets Changed:
Files in
view changes for bundle: exo-esm-modules-esmAssets Changed:
Files in
Files in
view changes for bundle: exo-full-iife-Exo-iifeAssets Changed:
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Loaderinto a standaloneAssetResidencyclass (src/resources/AssetResidency.ts) — the largest and highest-risk slice of the internal Loader-split effort (Slices 1/2:AssetTypeRegistryrefactor(resources): extract AssetTypeRegistry from Loader (Loader split, Slice 1/4) #414,AssetDecoderrefactor(resources): extract AssetDecoder from Loader (Loader split, Slice 2/4) #415, both merged).Loader.tsshrinks from 2319 to ~1200 lines. It keeps its call-shape dispatch methods (load/get/unload, the@internal_loadClaimed/_getClaimed/_releaseScope/_peekResourceentry pointsSceneLoader/serialize.tsdepend on) with their branching bodies intact, redirecting only the specific lines that touched now-moved state intothis._residency.*.pnpm docs:api:checkconfirms zero drift in the generated API docs.AssetResidencydispatchesonProgress/onLoaded/onErrorthroughLoader's ownSignalinstances, passed in at construction — the same pattern as Slice 2'sstoreResourcecallback injection.Test plan
pnpm typecheck— cleanpnpm test:core— 330/330 files, 5344 passed, 15 skipped, 0 failedpnpm verify:quick(typecheck + guides/examples/type-tests/packages typecheck + lint + format + docs:api:check) — clean, only pre-existing unrelated warningstest/resources/asset-residency.test.tsexercisesAssetResidencystandalone against realAssetTypeRegistry/AssetDecodercollaborators, including the multi-handle-fill and free-on-arrival invariants and the signal-dispatch boundarydestroy()deviation now documented as deliberate; two vacuous tests rewritten to test what they claim; ~60 lines of dropped correctness-rationale comments restored; a genuineundefined-vs-nullbehavior drift in a legacy lookup branch, caught and fixed) verified addressed by scoped re-reviews