Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions packages/server-runtime/src/middlewares/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ describe('route middleware', () => {

expect(collectDestinations(event)).toEqual(['label:env=prod'])
})
it('respects explicit empty destinations as an override', () => {
it('treats an explicit empty route destination list as the override', () => {
const event = createSparkNotifyEvent({
data: {
id: 'evt-3',
eventId: 'spark-3',
id: 'evt-override',
eventId: 'spark-override',
kind: 'ping',
urgency: 'soon',
headline: 'hello',
Expand All @@ -115,22 +115,36 @@ describe('route middleware', () => {
expect(collectDestinations(event)).toEqual([])
})

it('treats an explicit empty route destination list as the override', () => {
it('treats an explicit empty data destination list as the override', () => {
const event = createSparkNotifyEvent({
data: {
id: 'evt-override',
eventId: 'spark-override',
id: 'evt-data-empty',
eventId: 'spark-data-empty',
kind: 'ping',
urgency: 'soon',
headline: 'hello',
destinations: ['module:character'],
destinations: [],
},
route: { 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',
Expand Down
22 changes: 14 additions & 8 deletions packages/server-runtime/src/middlewares/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading