From a5d45bdd775d77e8774025d42b1aef7edb7ab9fa Mon Sep 17 00:00:00 2001 From: Ryanba <92616678+Gujiassh@users.noreply.github.com> Date: Mon, 4 May 2026 06:40:13 +0800 Subject: [PATCH] fix(server-runtime): preserve explicit empty data destinations (#1635) --- .../src/middlewares/route.test.ts | 46 ++++++++++++------- .../server-runtime/src/middlewares/route.ts | 22 +++++---- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/packages/server-runtime/src/middlewares/route.test.ts b/packages/server-runtime/src/middlewares/route.test.ts index 92d13c9ca..5446f8442 100644 --- a/packages/server-runtime/src/middlewares/route.test.ts +++ b/packages/server-runtime/src/middlewares/route.test.ts @@ -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', diff --git a/packages/server-runtime/src/middlewares/route.ts b/packages/server-runtime/src/middlewares/route.ts index e0d671f11..76235e332 100644 --- a/packages/server-runtime/src/middlewares/route.ts +++ b/packages/server-runtime/src/middlewares/route.ts @@ -25,6 +25,8 @@ export interface RouteContext { export type RouteMiddleware = (context: RouteContext) => RouteDecision | void +type DestinationList = Array + 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 & Partial>)) { +export function collectDestinations( + event: WebSocketEvent | (Omit & Partial>), +): DestinationList | undefined { if (event.route && 'destinations' in event.route) { return event.route.destinations } - const data = event.data as { destinations?: Array } | 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 }