feat(federation): resolveDmChannelId for alternate-origin DM ids
Resolves any raw DM channel ID (primary or alternate-origin local ID) to its primary dmChannels entry via dmAlternatives federatedId lookup. Returns null for unknown IDs. Used by the dm_message_created handler in a later commit to prevent phantom sidebar entries from alternate- origin deliveries (closes a pre-existing group-DM bug and supports post-failover routing).
This commit is contained in:
@@ -155,4 +155,61 @@ describe('spaceStore.dmAlternatives', () => {
|
|||||||
|
|
||||||
expect(useSpaceStore.getState().dmAlternatives.has('fed-solo')).toBe(false);
|
expect(useSpaceStore.getState().dmAlternatives.has('fed-solo')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolveDmChannelId returns the id itself if it is already a primary dmChannels entry', async () => {
|
||||||
|
const { resolveDmChannelId } = await import('./spaceStore');
|
||||||
|
useSpaceStore.getState().populateFromReady(
|
||||||
|
'',
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[makeDm('home-1', 'fed-aaa')],
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
expect(resolveDmChannelId('home-1')).toBe('home-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolveDmChannelId resolves an alternative id to the primary via federatedId', async () => {
|
||||||
|
const { resolveDmChannelId } = await import('./spaceStore');
|
||||||
|
// Home loads first → home-1 becomes the primary in dmChannels.
|
||||||
|
useSpaceStore.getState().populateFromReady(
|
||||||
|
'',
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[makeDm('home-1', 'fed-aaa')],
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
// Remote ready later → remote-1 recorded in dmAlternatives but deduped out of dmChannels.
|
||||||
|
useSpaceStore.getState().populateFromReady(
|
||||||
|
'https://remote.example',
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[makeDm('remote-1', 'fed-aaa')],
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
expect(resolveDmChannelId('remote-1')).toBe('home-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolveDmChannelId returns null if the id is unknown everywhere', async () => {
|
||||||
|
const { resolveDmChannelId } = await import('./spaceStore');
|
||||||
|
expect(resolveDmChannelId('nonexistent')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolveDmChannelId returns null if the alternative points to a federatedId no longer in dmChannels', async () => {
|
||||||
|
const { resolveDmChannelId } = await import('./spaceStore');
|
||||||
|
useSpaceStore.getState().populateFromReady(
|
||||||
|
'https://remote.example',
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[makeDm('remote-1', 'fed-aaa')],
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
// Simulate the dmChannels entry getting removed without clearing dmAlternatives —
|
||||||
|
// resolveDmChannelId should gracefully return null rather than a stale pointer.
|
||||||
|
useSpaceStore.setState({ dmChannels: [] });
|
||||||
|
expect(resolveDmChannelId('remote-1')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -924,6 +924,33 @@ export function getChannelOrigin(channelId: string): string {
|
|||||||
return useSpaceStore.getState().channelOriginMap.get(channelId) ?? '';
|
return useSpaceStore.getState().channelOriginMap.get(channelId) ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a raw DM channel ID to its primary `dmChannels` entry ID.
|
||||||
|
*
|
||||||
|
* - If `rawId` is already a primary entry: returns `rawId` unchanged.
|
||||||
|
* - If `rawId` is recorded in `dmAlternatives` as an alternate-origin local ID
|
||||||
|
* for a DM whose primary is present in `dmChannels`: returns the primary's ID.
|
||||||
|
* - Otherwise: returns `null` (unknown ID — caller should no-op).
|
||||||
|
*
|
||||||
|
* Used by:
|
||||||
|
* - `dm_message_created` WS handler to route messages arriving from alternate
|
||||||
|
* origins to the primary entry (§3.11 of the failover spec).
|
||||||
|
* - Future DM WS handlers that need to dedup alternate-origin deliveries.
|
||||||
|
*/
|
||||||
|
export function resolveDmChannelId(rawId: string): string | null {
|
||||||
|
const { dmChannels, dmAlternatives } = useSpaceStore.getState();
|
||||||
|
if (dmChannels.some(dm => dm.id === rawId)) return rawId;
|
||||||
|
|
||||||
|
for (const [federatedId, byOrigin] of dmAlternatives) {
|
||||||
|
for (const localId of byOrigin.values()) {
|
||||||
|
if (localId !== rawId) continue;
|
||||||
|
const primary = dmChannels.find(dm => dm.federatedId === federatedId);
|
||||||
|
return primary ? primary.id : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── API client resolution ────────────────────────────────────────────────────
|
// ─── API client resolution ────────────────────────────────────────────────────
|
||||||
// The actual resolver is registered by instanceStore on import, avoiding a
|
// The actual resolver is registered by instanceStore on import, avoiding a
|
||||||
// circular dependency (instanceStore → useWebSocket → chatStore → spaceStore).
|
// circular dependency (instanceStore → useWebSocket → chatStore → spaceStore).
|
||||||
|
|||||||
Reference in New Issue
Block a user