Skip to content

fix: add ensureTypeLoaded() before registry .get() calls for lazy types#1116

Merged
stack72 merged 1 commit intomainfrom
fix/lazy-registry-ensure-type-loaded
Apr 6, 2026
Merged

fix: add ensureTypeLoaded() before registry .get() calls for lazy types#1116
stack72 merged 1 commit intomainfrom
fix/lazy-registry-ensure-type-loaded

Conversation

@stack72
Copy link
Copy Markdown
Contributor

@stack72 stack72 commented Apr 6, 2026

Summary

PR #1089 introduced lazy per-bundle loading for vault, datastore, driver, and report registries. This caused a regression: get() only returns fully-loaded types (from this.types), returning undefined for lazy entries (in this.lazyTypes) — but many consumer code paths call get() without first calling ensureTypeLoaded() to promote the lazy entry.

This broke 11 UATs:

  • Datastore (6 failures): Unknown datastore type "@swamp/s3-datastore" / same for GCS
  • Vault (5 failures): Unknown vault type: @test/mock-vault — vault create, vault put, and extension vault lifecycle all fail

Root cause

The registries have two maps: types (fully loaded) and lazyTypes (indexed but not imported). After PR #1089:

  • has() returns true for both maps — masking the problem in conditional checks
  • get() only checks types — returning undefined for lazy entries
  • ensureLoaded() only populates lazyTypes (builds the index), it does NOT promote entries to types
  • ensureTypeLoaded(type) is required to import the specific bundle and move the entry from lazyTypes to types

The model registry was already correct — resolveModelType() in extension_auto_resolver.ts calls await modelRegistry.ensureTypeLoaded(type) before get(). The vault, datastore, driver, and report registries needed the same treatment.

Changes

Added await registry.ensureTypeLoaded(type) before every .get() call across all four registries (15 files, 11 code paths):

# File Function Change
1 extension_auto_resolver.ts resolveVaultType() Added ensureTypeLoaded before has() check
2 extension_auto_resolver.ts resolveDatastoreType() Same pattern
3 vault_service.ts fromRepository() Added ensureTypeLoaded before registerVault()
4 libswamp/vaults/create.ts resolveExtensionVaultType callback Added ensureTypeLoaded before has() check
5 libswamp/datastores/sync.ts createDatastoreSyncDeps() Added ensureTypeLoaded before .get()
6 libswamp/datastores/status.ts verifyHealth callback Added ensureTypeLoaded before .get()
7 libswamp/datastores/setup.ts datastoreSetupExtension() Added ensureTypeLoaded before .get()
8 cli/repo_context.ts resolveCustomProvider() Added ensureTypeLoaded alongside existing ensureLoaded()
9 cli/resolve_datastore.ts 4 locations in parseDatastoreEnvVar and resolveDatastoreConfig Added ensureTypeLoaded before each .get()
10 method_execution_service.ts Driver lookup ~L646 Added ensureTypeLoaded before .get()
11 reports/describe.ts + search.ts getReport callback Made async with ensureTypeLoaded before .get(); updated interface, callers, CLI commands, and tests

Why ensureLoaded() alone is insufficient

After PR #1089, ensureLoaded() calls buildIndex() which only registers lazy entries via registerLazy(). It does not import bundles or populate this.types. Only ensureTypeLoaded(type) triggers the typeLoader to import the specific bundle and call promoteFromLazy(), moving the entry from lazyTypes to types where get() can find it.

Test Plan

  • deno check — all types pass
  • deno lint — clean
  • deno fmt --check — clean
  • deno run test — 4174 tests pass, 0 failures
  • Manual verification with compiled binary:
    • swamp repo init in /tmp directory
    • swamp extension pull @swamp/aws-sm — installed extension vault
    • swamp vault create @swamp/aws-sm my-aws-vault --config '{"region":"us-east-1"}'succeeded (would have failed with "Unknown vault type" before fix)
    • swamp extension pull @swamp/s3-datastore — installed extension datastore
    • swamp datastore setup extension @swamp/s3-datastore --config '{"bucket":"test","region":"us-east-1"}'reached S3 access check (would have failed with "not registered or has no provider" before fix)

🤖 Generated with Claude Code

PR #1089 introduced lazy per-bundle loading for vault, datastore, driver,
and report registries. The get() method only returns fully-loaded types,
returning undefined for lazy entries — but many consumer code paths call
get() without first calling ensureTypeLoaded() to promote lazy entries.

This caused extension types like @swamp/s3-datastore and @swamp/aws-sm
to be treated as unknown, breaking 11 UATs across vault and datastore
operations.

The fix adds await registry.ensureTypeLoaded(type) before every .get()
call across all four registries, matching the pattern already used by
resolveModelType() in extension_auto_resolver.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

CLI UX Review

Blocking

None.

Suggestions

None.

Verdict

PASS — This PR is a pure internal bug fix. No flags, help text, error messages, JSON output shapes, or log-mode output were changed. The only user-visible effect is that commands involving extension vaults, datastores, drivers, and reports now work correctly instead of failing with "Unknown vault type" / "Unknown datastore type" errors. The getReport interface change from sync to async is an internal contract update with no observable UX difference.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Code Review

Clean, well-scoped bug fix. The pattern of adding ensureTypeLoaded() before .get() is correct and consistent with the existing model registry approach in extension_auto_resolver.ts. All call sites are covered.

Blocking Issues

None.

Suggestions

  1. getVaultTypeInfo in libswamp/vaults/create.ts:83 — This synchronous .get() call relies on resolveExtensionVaultType (line 127) being called first to ensure the type is loaded. The ordering is correct today, but the dependency is implicit. Consider making getVaultTypeInfo async with its own ensureTypeLoaded for defensiveness, matching the pattern used for getReport. Not blocking since the current call ordering is safe.

  2. Pre-existing import boundary violation in src/cli/resolve_datastore.ts:44maybeAutoUpdateDatastoreExtension is imported from ../libswamp/extensions/datastore_auto_update.ts instead of ../libswamp/mod.ts (which does export it). Not introduced by this PR.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Adversarial Review

Critical / High

None found.

Medium

None found.

Low

  1. src/libswamp/reports/search.ts:191-199 — Sequential awaits in label filter loop. The conversion from .filter() to a for...of loop with await deps.getReport() makes report lookups sequential. If there are many candidates with distinct report names, this does N sequential loads instead of parallel. In practice this is negligible because ensureTypeLoaded returns immediately for already-loaded types (all reports likely share a small set of types), so only the first call per type incurs actual work. Not a real issue.

  2. src/libswamp/vaults/create.ts:83getVaultTypeInfo is synchronous and calls vaultTypeRegistry.get() without ensureTypeLoaded. This looks potentially missed, but it's actually safe: resolveExtensionVaultType (line 77-81) is always called first in the vaultCreate generator (line 127), and that function now calls ensureTypeLoaded. By the time getVaultTypeInfo is called at line 130, the type is already promoted. The ordering dependency is implicit rather than enforced by the type system, but given the generator's sequential flow, this works correctly.

Verdict

PASS — This is a straightforward, mechanical fix applied consistently across all four registries. Every get() call site either has a preceding ensureTypeLoaded or is in a context where the type was already promoted (loader internals, or sequenced after a prior ensureTypeLoaded in the same flow). The async conversion of ReportDescribeDeps.getReport and ReportSearchDeps.getReport is correct, and the label filter refactor from .filter() to an imperative loop properly handles the new async signature. The ensureTypeLoaded method is safe to call with non-existent types (returns silently), so there are no new error paths introduced.

@stack72 stack72 merged commit 12f307f into main Apr 6, 2026
10 checks passed
@stack72 stack72 deleted the fix/lazy-registry-ensure-type-loaded branch April 6, 2026 17:25
stack72 added a commit that referenced this pull request Apr 6, 2026
Adds 10 tests verifying the lazy registry promotion contract introduced
by PR #1089 and fixed by PR #1116:

Category 1 (8 tests, fresh instances): Documents the registry invariant
that get() returns undefined for lazy entries and ensureTypeLoaded()
must be called first to promote them. Covers all four registries (vault,
datastore, driver, report).

Category 2 (2 tests, global singletons): Verifies that resolveVaultType()
and resolveDatastoreType() promote lazy entries before returning, so
callers can safely use get() afterwards. These tests would have caught
the PR #1089 regression — they fail without the ensureTypeLoaded() calls
added in PR #1116.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
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