It's getting to the point where we need to talk API again. In making this new object caching work, sometimes we are "newing" a card when I already have a card object in the object cache. We could just add a def new ... in Card that returns the object in just that case and continues the normal coarse into initialize in the other cases. Cardname works similarly when you pass new a Cardname. It simplifies deciding when you have one vs. need to create it for real. Now Card.fetch_or_new would be the equivalent of 'name string'.to_cardname.card, which does a card_with_fetch on the cardinfo object.
It's this flow that I've given us the potential to really optimize. If I've seen a card before, the string to card translation, with missing and virtual status is very fast. So in "string".to_cardname.card, the steps are: 1) create the Cardname object, a) from scratch, when I've never seen this key before, b) from new variant and it finds the CardInfo object from the NAME2KEY hash, and c) it's already a Cardname, so just return it. 2) card is delegated to the CardInfo found in 1, calling fetch_with_cache. a) return if from the CardInfo @card attribute or b) Always populate the card and set the flags (missing and virtual). The case of 1.c and 2.b is a type check, and two attribute lookups.
The card invalidation is very simple, just clear the cardinfo.card attribute to nil and it will refetch from the external cache or db. I also think this eliminates most of the motivation for things like skip_defaults and skip_after_fetch. If you are in Card#initialize there is a very good reason, and not just some simple tests that I can do with the cardname and type.