File size: 6,312 Bytes
fa9c65f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | /**
* Which panel key a generic NewsPanel takes for a CANONICAL_FEEDS category.
*
* A feed-category key is not automatically free to use as a panel key. Several
* categories share a name with a DATA panel registered earlier in the same pass
* β `markets` (MarketPanel), `commodities` (CommoditiesPanel), `crypto`
* (CryptoPanel), `economic` (EconomicPanel), `supply-chain` (SupplyChainPanel).
* The registration dedup guard makes the second claim on a key a silent no-op,
* so a NewsPanel asking for the colliding key is simply never created. It has to
* take `${key}-news` instead β which is exactly what the `markets-news` /
* `crypto-news` / `economic-news` / `commodities-news` entries in the panel
* catalog are for.
*
* That collision used to be an enumerated set in panel-layout
* (`COLLIDING_NEWS_PANEL_KEYS`) and it omitted `commodities`: the finance
* variant shipped a "Commodities News" settings toggle and a mission preset
* (`finCommodities`) that both referenced a panel nothing ever created (#5871).
* #5376 hit the mirror image of this on the data-resolution side and replaced
* its constant with a registry derived at registration time; this is the same
* treatment for the panel-creation side. The caller knows, synchronously,
* whether something already owns a key β so ask it instead of keeping a
* hand-maintained list in sync with the panel registry.
*/
/** The `has(key)` surface of a Set or Map β all this module needs from either. */
export interface KeyMembership {
has: (key: string) => boolean;
}
export interface NewsPanelKeyLookups {
/** `true` when CANONICAL_FEEDS carries a feed list under this key. */
isFeedCategory: (key: string) => boolean;
/** `true` when a NewsPanel has already been registered for this category. */
hasNewsPanel: (categoryKey: string) => boolean;
/**
* `true` when some panel already owns this panel key β created, or registered
* lazily and not yet loaded. Both count: a lazy registration reserves the key
* long before `ctx.panels` gains an entry, and that reservation is what makes
* the second claim a no-op.
*/
isPanelKeyClaimed: (panelKey: string) => boolean;
/** `true` when `panelSettings` carries an entry for this panel key. */
hasPanelSetting: (panelKey: string) => boolean;
}
/**
* The panel key to register a NewsPanel for `categoryKey` under, or `null` when
* no NewsPanel should be created for it.
*
* `null` covers every reason the pass has to skip a category: it carries no feed
* list, it already has a NewsPanel, the key it resolves to is already taken (so
* the registration would be a no-op), or that key has no settings entry β which
* is how a data-panel collision with no `${key}-news` catalog entry stays
* invisible rather than spawning a phantom panel under the data panel's own
* settings entry.
*
* Returning the key that will actually be claimed (rather than the key we would
* *like* to claim) is what makes this answerable in a test: "every `*-news`
* settings entry is reachable from some feed category" is checkable only if the
* caller can tell a resolution that lands a panel from one that silently does
* nothing.
*/
export function newsPanelKeyForCategory(
categoryKey: string,
lookups: NewsPanelKeyLookups,
): string | null {
if (!lookups.isFeedCategory(categoryKey)) return null;
if (lookups.hasNewsPanel(categoryKey)) return null;
const panelKey = lookups.isPanelKeyClaimed(categoryKey) ? `${categoryKey}-news` : categoryKey;
if (lookups.isPanelKeyClaimed(panelKey)) return null;
if (!lookups.hasPanelSetting(panelKey)) return null;
return panelKey;
}
/**
* Whether `panelSettings` carries an entry for `key`.
*
* Presence, not truthiness β this is what every panel registration in
* panel-layout gates on (`shouldCreatePanel`), so the news pass has to ask the
* same question or it would resolve to a key the registration then refuses.
*/
export function hasPanelSettingEntry(panelSettings: object, key: string): boolean {
return Object.prototype.hasOwnProperty.call(panelSettings, key);
}
/**
* Live registration state the CANONICAL_FEEDS pass reads. Field names mirror the
* panel-layout members they are fed from.
*/
export interface NewsPanelRegistrationState {
/** `CANONICAL_FEEDS` β a key with an array value is a feed category. */
canonicalFeeds: Record<string, unknown>;
/** `ctx.panels` β panels already created. */
panels: Record<string, unknown>;
/** `lazyPanelRegistrations` β keys reserved by a registration that has not loaded yet. */
lazyPanelRegistrations: KeyMembership;
/** `ctx.newsCategoryPanelKeys` β categories whose NewsPanel registration was claimed. */
newsCategoryPanelKeys: KeyMembership;
/** `ctx.panelSettings` β the effective panel catalog for this session. */
panelSettings: object;
/** Keys owned by a panel registered AFTER the pass (`LATE_REGISTERED_PANEL_KEYS`). */
lateRegisteredPanelKeys: KeyMembership;
}
/**
* Bind `newsPanelKeyForCategory` to live registration state.
*
* This adapter is the half a resolver test could otherwise never reach. The
* resolver only asks *questions*; whether the answers describe reality is
* decided here β most load-bearingly that a key counts as claimed when it is
* merely REGISTERED (`lazyPanelRegistrations`) and not yet created. That is the
* whole mechanism of #5871: a lazy registration reserves the key long before
* `ctx.panels` gains an entry, so an adapter that consulted only `ctx.panels`
* would report `commodities` unclaimed, hand back the data panel's own key, and
* let the registration no-op exactly as the hardcoded set did. Building it here
* rather than inline in `createPanels()` is what lets the guard bind it.
*/
export function newsPanelKeyLookupsFor(state: NewsPanelRegistrationState): NewsPanelKeyLookups {
return {
isFeedCategory: (key) => Array.isArray(state.canonicalFeeds[key]),
hasNewsPanel: (categoryKey) => state.newsCategoryPanelKeys.has(categoryKey),
isPanelKeyClaimed: (panelKey) =>
state.lateRegisteredPanelKeys.has(panelKey)
|| state.panels[panelKey] != null
|| state.lazyPanelRegistrations.has(panelKey),
hasPanelSetting: (panelKey) => hasPanelSettingEntry(state.panelSettings, panelKey),
};
}
|