Skip to content

Add Registry#resolve_missing — read-through resolution of unknown types - #26

Open
fractaledmind wants to merge 1 commit into
zarpay:mainfrom
fractaledmind:feat/registry-resolve-missing
Open

Add Registry#resolve_missing — read-through resolution of unknown types#26
fractaledmind wants to merge 1 commit into
zarpay:mainfrom
fractaledmind:feat/registry-resolve-missing

Conversation

@fractaledmind

Copy link
Copy Markdown

Why

The registry is per-process, in-memory state. When types are registered at runtime from a source of truth (e.g. database rows), a process that booted before a type was created — or that never created the row itself — raises UnknownType on lookup even though the type is derivable. Consumers work around this by registering defensively at each call site, which is easy to forget (a single missed site terminally fails otherwise-complete work).

Real-world manifestation in a consumer: an app registers each token's Amount type via a Token after_commit (per-process) plus a boot sweep. On a token's first-ever use, a different worker holds the DB row without its type and raises Amount::Registry::UnknownType.

What

Registry#resolve_missing { |symbol| ... } registers an optional resolver invoked on a #lookup miss before UnknownType is raised. The block receives the missing symbol, registers it as a side effect (from wherever the source of truth lives), and lookup retries once. The registry becomes a read-through cache.

Amount.registry.resolve_missing do |symbol|
  Token.for_amount_key(symbol)&.register_amount_type
end
  • Runs the resolver without the registry lock held (it calls #register, which locks), guarded against per-thread re-entrancy so a resolver that re-triggers the same miss can't recurse forever.
  • A resolver that raises is swallowed; the original UnknownType is surfaced (a resolver failure never masks the real error).
  • All construction paths (Amount.new, conversions, generated constructors, register_default_rate) funnel through #lookup, so one resolver covers them all.
  • #clear! resets the resolver too.

Opt-in and fully backward-compatible: with no resolver set, #lookup behaves exactly as before.

Tests

Six new minitest cases in test/test_amount.rb: resolve-on-miss, still-raises-when-resolver-registers-nothing, raising-resolver-surfaces-original-error, re-entrancy guard (runs once, no recursion), no-block clears, and clear! resets. Full suite green (test_amount, test_active_record, test_registry_generator, both RSpec matcher specs); RuboCop clean. Version bumped to 0.0.9 with a CHANGELOG entry.


Opened from a fork (fractaledmind:feat/registry-resolve-missing) — feel free to re-push the branch into zarpay/amounts directly if you'd prefer it on the canonical repo before merging/releasing 0.0.9.

🤖 Generated with Claude Code

The registry is per-process, in-memory state. When types are created at
runtime (e.g. from database rows), a process that booted before a type was
registered — or that never created the row itself — raises UnknownType on
lookup even though the type is derivable. Consumers worked around this by
registering defensively at each call site.

resolve_missing lets the application register a resolver invoked on a lookup
miss BEFORE UnknownType is raised: it receives the missing symbol, registers
it as a side effect (from wherever the source of truth lives), and lookup
retries once. The registry becomes a read-through cache.

- Runs the resolver without the registry lock held (it calls #register, which
  locks), guarded against per-thread re-entrancy so a resolver that re-triggers
  the same miss can't recurse forever.
- A resolver that raises is swallowed; the original UnknownType is surfaced.
- All construction paths (Amount.new, conversions, generated constructors,
  register_default_rate) funnel through #lookup, so one resolver covers them all.
- #clear! resets the resolver too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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