fix(server-runtime): preserve explicit empty data destinations (#1635)

This commit is contained in:
Ryanba
2026-05-04 06:40:13 +08:00
committed by GitHub
parent af9c0203e7
commit a5d45bdd77
2 changed files with 44 additions and 24 deletions
@@ -99,22 +99,6 @@ describe('route middleware', () => {
expect(collectDestinations(event)).toEqual(['label:env=prod'])
})
it('respects explicit empty destinations as an override', () => {
const event = createSparkNotifyEvent({
data: {
id: 'evt-3',
eventId: 'spark-3',
kind: 'ping',
urgency: 'soon',
headline: 'hello',
destinations: ['module:character'],
},
route: { destinations: [] },
})
expect(collectDestinations(event)).toEqual([])
})
it('treats an explicit empty route destination list as the override', () => {
const event = createSparkNotifyEvent({
data: {
@@ -131,6 +115,36 @@ describe('route middleware', () => {
expect(collectDestinations(event)).toEqual([])
})
it('treats an explicit empty data destination list as the override', () => {
const event = createSparkNotifyEvent({
data: {
id: 'evt-data-empty',
eventId: 'spark-data-empty',
kind: 'ping',
urgency: 'soon',
headline: 'hello',
destinations: [],
},
route: undefined,
})
expect(collectDestinations(event)).toEqual([])
})
it('ignores primitive data payloads when checking destinations', () => {
const event = {
type: 'spark:notify',
data: 'not-an-object',
metadata: {
source: { kind: 'plugin', plugin: { id: 'server-runtime' }, id: 'test' },
event: { id: 'evt-primitive' },
},
route: undefined,
} as unknown as WebSocketBaseEvent<'spark:notify', WebSocketEvents['spark:notify'], any>
expect(collectDestinations(event)).toBeUndefined()
})
it('matches destinations by label selector', () => {
const peer = createPeer({
id: 'peer-2',
@@ -25,6 +25,8 @@ export interface RouteContext {
export type RouteMiddleware = (context: RouteContext) => RouteDecision | void
type DestinationList = Array<string | RouteTargetExpression>
function getPeerLabels(peer: AuthenticatedPeer) {
return {
...peer.identity?.plugin?.labels,
@@ -118,25 +120,29 @@ export function createPolicyMiddleware(policy: RoutingPolicy): RouteMiddleware {
}
/**
* Resolves the destinations attached to an event.
* Collects explicit route destinations from the route envelope or event payload.
*
* Use when:
* - Route-level destinations should override payload-level destinations
* - Delivery logic needs to distinguish between "broadcast" and "explicitly send nowhere"
* - Routing middleware needs the effective destination override for a websocket event
* - Callers must preserve explicit empty destination lists instead of falling back to broadcast
*
* Expects:
* - An explicit empty `route.destinations` array is a meaningful override
* - A websocket event whose `route.destinations` or `data.destinations` may be present
* - `data.destinations` is only treated as valid when it is an array-shaped override
*
* Returns:
* - The route destinations, payload destinations, or `undefined` when the event is unrestricted
* - The explicit destination list when present
* - `undefined` when no destination override was provided
*/
export function collectDestinations(event: WebSocketEvent | (Omit<WebSocketEvent, 'metadata'> & Partial<Pick<WebSocketEvent, 'metadata'>>)) {
export function collectDestinations(
event: WebSocketEvent | (Omit<WebSocketEvent, 'metadata'> & Partial<Pick<WebSocketEvent, 'metadata'>>),
): DestinationList | undefined {
if (event.route && 'destinations' in event.route) {
return event.route.destinations
}
const data = event.data as { destinations?: Array<string | RouteTargetExpression> } | undefined
if (data?.destinations?.length) {
const data = event.data as unknown
if (typeof data === 'object' && data !== null && 'destinations' in data && Array.isArray(data.destinations)) {
return data.destinations
}