From ab8e40acf0afec713747e8cf954d273550677657 Mon Sep 17 00:00:00 2001 From: kemerava Date: Fri, 27 Mar 2026 10:02:29 -0400 Subject: [PATCH 1/3] fdc3.addContextListener() accepts array type --- CHANGELOG.md | 1 + .../api/addContextListenerRequest.schema.json | 26 ++++++++++++- packages/fdc3-standard/src/api/Channel.ts | 11 ++++++ .../fdc3-standard/src/api/DesktopAgent.ts | 11 ++++++ .../docs/api/conformance/App-Channel-Tests.md | 18 +++++++++ website/docs/api/conformance/Basic-Tests.md | 3 ++ .../api/conformance/User-Channel-Tests.md | 18 ++++++++- website/docs/api/ref/Channel.md | 39 ++++++++++++++++++- website/docs/api/ref/DesktopAgent.md | 22 +++++++++++ 9 files changed, 146 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 205ef1679..ebec10f44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Added `clearContext` function and associated `contextClearedEvent` to the `Channel` API, to be able to clear specific or all context types from the channel. ([#1379](https://github.com/finos/FDC3/pull/1379)) * Added Conformance tests for FDC3 2.2 ([#1586](https://github.com/finos/FDC3/pull/1586)) * Added custom mocha test runner for conformance tests to better display test progress. ([#1769](https://github.com/finos/FDC3/pull/1769)) +* Added support for arrays of context types in `addContextListener` methods across DesktopAgent, Channel, and PrivateChannel interfaces. This allows applications to listen for multiple specific context types with a single listener registration, improving code conciseness and performance. The array may contain `null` to listen for all context types in addition to specific types. ([#1646](https://github.com/finos/FDC3/issues/1646)) ### Changed diff --git a/packages/fdc3-schema/schemas/api/addContextListenerRequest.schema.json b/packages/fdc3-schema/schemas/api/addContextListenerRequest.schema.json index dfbc03ad5..e31352a28 100644 --- a/packages/fdc3-schema/schemas/api/addContextListenerRequest.schema.json +++ b/packages/fdc3-schema/schemas/api/addContextListenerRequest.schema.json @@ -54,11 +54,35 @@ "type": "null" } ] + }, + "contextTypes": { + "title": "Context types", + "description": "Array of context types to listen for. May contain `null` to listen to all context types in addition to specific types. When `null` is present, other context types are ignored as the listener will receive all context types.", + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "minItems": 1 } }, "additionalProperties": false, "required": [ - "channelId", "contextType" + "channelId" + ], + "oneOf": [ + { + "required": ["contextType"] + }, + { + "required": ["contextTypes"] + } ] } } diff --git a/packages/fdc3-standard/src/api/Channel.ts b/packages/fdc3-standard/src/api/Channel.ts index c49c23c2d..c5fe19959 100644 --- a/packages/fdc3-standard/src/api/Channel.ts +++ b/packages/fdc3-standard/src/api/Channel.ts @@ -77,6 +77,17 @@ export interface Channel { */ addContextListener(contextType: string | null, handler: ContextHandler): Promise; + /** + * Adds a listener for incoming contexts using an array of context types. + * + * Pass multiple context types to listen for several types with one listener. + * If the array contains `null`, it's treated as if `null` was passed directly (listens to all context types), so any other context types in the same array are ignored as the listener will receive all context types. + * + * See the single `contextType` overload above for full behavior details. + * + */ + addContextListener(contextTypes: (string | null)[], handler: ContextHandler): Promise; + /** * Clears context from the channel, and triggers the event listener on the `contextCleared` event to notify existing listeners that the context was cleared. Listeners added to the channel and calls to [`getCurrentContext`](#getcurrentcontext) will not receive any existing context until new context is broadcast to the channel. * diff --git a/packages/fdc3-standard/src/api/DesktopAgent.ts b/packages/fdc3-standard/src/api/DesktopAgent.ts index 9b930aa0d..110f0cbf0 100644 --- a/packages/fdc3-standard/src/api/DesktopAgent.ts +++ b/packages/fdc3-standard/src/api/DesktopAgent.ts @@ -380,6 +380,17 @@ export interface DesktopAgent { */ addContextListener(contextType: ContextType | null, handler: ContextHandler): Promise; + /** + * Adds a listener for incoming contexts using an array of context types. + * + * Pass multiple context types to listen for several types with one listener. + * If the array contains `null`, it's treated as if `null` was passed directly (listens to all context types), so any other context types in the same array are ignored as the listener will receive all context types. + * + * See the single `contextType` overload above for full behavior details. + * + */ + addContextListener(contextTypes: (ContextType | null)[], handler: ContextHandler): Promise; + /** * Register a handler for events from the Desktop Agent. Whenever the handler function * is called it will be passed an event object with details related to the event. diff --git a/website/docs/api/conformance/App-Channel-Tests.md b/website/docs/api/conformance/App-Channel-Tests.md index 60f0628fc..77f12458f 100644 --- a/website/docs/api/conformance/App-Channel-Tests.md +++ b/website/docs/api/conformance/App-Channel-Tests.md @@ -47,6 +47,24 @@ hide_title: true - `ACFilteredContext4`: Perform above test, except that after creating the channel **A** creates another channel with a further _different_ channel id and adds a further context listener to it. Ensure that **A** is still able to receive context on the first channel (i.e. it is unaffected by the additional channel) and does NOT receive anything on the second channel. - `ACUnsubscribe`: Perform above test, except that after creating the channel **A** then `unsubscribe()`s the listener it added to the channel. Check that **A** does NOT receive anything. +## Array Context Listeners + +In FDC3 3.0, support for arrays of context types was added to `addContextListener`, allowing applications to listen for multiple specific context types with a single listener registration. + +### Array Context Listeners + +| App | Step | Details | +|-----|-------------------------|-----------------------------------------------------------------| +| A | 1. Retrieve `Channel` | Retrieve a `Channel` object representing an 'App' channel called `test-channel` using:
`const testChannel = await fdc3.getOrCreateChannel("test-channel")` | +| A | 2. Add Context Listener | Add a context listener for multiple types:
`await testChannel.addContextListener(["fdc3.instrument", "fdc3.contact"],handler)` | +| B | 3. Retrieve `Channel` | Retrieve a `Channel` object representing the same 'App' channel A did (`test-channel`)| +| B | 4. Broadcast | B broadcasts:
1.`testChannel.broadcast()`
2. `testChannel.broadcast()`
3. `testChannel.broadcast()`| +| A | 5. Receive Context | Handler receives `fdc3.instrument` and `fdc3.contact` contexts only.
Verify that `fdc3.portfolio` is NOT received. | + +- `ACArrayContextListeners1`: Perform above test to verify array filtering works correctly. +- `ACArrayNullContext1`: Perform above test, but in step2 use `await testChannel.addContextListener(["fdc3.instrument", null],handler)` to verify arrays with null behave like direct null (receives ALL contexts). +- `ACArrayEmptyContext1`: Attempt to create a context listener with an empty array `await testChannel.addContextListener([],handler)` and verify context is not recieved. + ### App Channel History | App | Step | Details | diff --git a/website/docs/api/conformance/Basic-Tests.md b/website/docs/api/conformance/Basic-Tests.md index 9e66a80c3..4e828d393 100644 --- a/website/docs/api/conformance/Basic-Tests.md +++ b/website/docs/api/conformance/Basic-Tests.md @@ -26,6 +26,9 @@ _These are some basic sanity tests implemented in the FDC3 Conformance Framework - `BasicCL1`: A context listener can be created for a specific context type by calling `fdc3.addContextListener("fdc3.contact",)`. A `Listener` object is returned and can be used to remove the listener again by calling its `unsubscribe` function. - `BasicCL2`: An **unfiltered** context listener can be created by calling `fdc3.addContextListener(null,)`. A `Listener` object is returned and can be used to remove the listener again by calling its `unsubscribe` function. +- `BasicCL3`: A context listener can be created for multiple specific context types by calling `fdc3.addContextListener(["fdc3.instrument", "fdc3.contact"],)`. A `Listener` object is returned and can be used to remove the listener again by calling its `unsubscribe` function. The listener should only receive contexts matching the specified types. +- `BasicCL4`: A context listener created with an array containing `null` (e.g. `["fdc3.instrument", null]`) should behave as if `null` was passed directly, receiving all context types. A `Listener` object is returned and can be used to remove the listener again by calling its `unsubscribe` function. +- `BasicCL5`: Attempting to create a context listener with an empty array should be ignored. - `BasicIL1`: An intent listener can be created for a specific intent by calling `fdc3.addIntentListener(,)`. A `Listener` object is returned and can be used to remove the listener again by calling its `unsubscribe` function. - `BasicGI1`: An `ImplementationMetadata` object can be retrieved, to find out the version of FDC3 that is in use along with details of the provider, by calling: - `await fdc3.getInfo()`. The FDC3 version should match the API version being tested for conformance. diff --git a/website/docs/api/conformance/User-Channel-Tests.md b/website/docs/api/conformance/User-Channel-Tests.md index 075b52061..8abb5d0f6 100644 --- a/website/docs/api/conformance/User-Channel-Tests.md +++ b/website/docs/api/conformance/User-Channel-Tests.md @@ -83,4 +83,20 @@ As the method of setting the user channel is user interactive, it is either diff | A | 5. Receive Context | A's `fdc3.contact` object matches the one broadcast by B, both handlers from step 1 are triggered, and broadcast arrives on the correct listener. | - UCMultipleOverlappingListeners1: Perform above test -- UCMultipleOverlappingListeners2: Perform above test, but instead of _untyped_ context listener, in step 2, use `fdc3.instrument` (handler should remain different) \ No newline at end of file +- UCMultipleOverlappingListeners2: Perform above test, but instead of _untyped_ context listener, in step 2, use `fdc3.instrument` (handler should remain different) + +## Array Context Listeners + +In FDC3 3.0, support for arrays of context types was added to `addContextListener`, allowing applications to listen for multiple specific context types with a single listener registration. + +| App | Step | Details | +|-----|-----------------------|----------------------------------------------------------------------------------| +| A | 1. addContextListener | A adds a context listener for multiple types using `addContextListener(["fdc3.instrument", "fdc3.contact"],handler)`.
A promise resolving to a `Listener` object is returned
Check that this has an `unsubscribe` function.| +| A | 2. joinUserChannel | A joins the first available user channel using standard process.| +| B | 3. joinUserChannel | B joins the same channel as A. | +| B | 4. Broadcast | B broadcasts:
1.`fdc3.broadcast()`.
2. `fdc3.broadcast()`
3. `fdc3.broadcast()` | +| A | 5. Receive Context | A receives both `fdc3.instrument` and `fdc3.contact` objects, matching the ones broadcast by B.
Check that `fdc3.portfolio` is NOT received. | + +- `UCArrayContextListeners1`: Perform above test to verify array filtering works correctly. +- `UCArrayNullContext1`: Perform above test, but in step1 use `addContextListener(["fdc3.instrument", null],handler)` to verify arrays with null behave like direct null (receives ALL contexts). +- `UCArrayEmptyContext1`: Attempt to create a context listener with an empty array `addContextListener([],handler)` and verify context is not received. \ No newline at end of file diff --git a/website/docs/api/ref/Channel.md b/website/docs/api/ref/Channel.md index 954cc4774..c9b3d4956 100644 --- a/website/docs/api/ref/Channel.md +++ b/website/docs/api/ref/Channel.md @@ -36,6 +36,7 @@ interface Channel { broadcast(context: Context): Promise; getCurrentContext(contextType?: string): Promise; addContextListener(contextType: string | null, handler: ContextHandler): Promise; + addContextListener(contextTypes: (string | null)[], handler: ContextHandler): Promise; clearContext(contextType?: string): Promise; addEventListener(type: string | null, handler: EventHandler): Promise; @@ -214,7 +215,11 @@ DisplayMetadata can be used to provide display hints for User Channels intended ```ts +// Single context type public addContextListener(contextType: string | null, handler: ContextHandler): Promise; + +// Array of context types +public addContextListener(contextTypes: (string | null)[], handler: ContextHandler): Promise; ``` @@ -238,6 +243,8 @@ func (ch *Channel) AddContextListener(contextType string, handler ContextHandler Adds a listener for incoming contexts of the specified _context type_ whenever a broadcast happens on this channel. +Alternatively, you can pass an array of context types to listen for multiple specific types at once. If the array contains `null`, it will be treated as if `null` was passed directly (meaning listen to all context types), so any other context types are ignored as the listener will receive all context types. Empty arrays will be ignored. + If, when this function is called, the channel already contains context that would be passed to the listener it is NOT called or passed this context automatically (this behavior differs from that of the [`fdc3.addContextListener`](DesktopAgent#addcontextlistener) function). Apps wishing to access to the current context of the channel should instead call the [`getCurrentContext(contextType)`](#getcurrentcontext) function. Optional metadata about each context message received, including the app that originated the message, SHOULD be provided by the desktop agent implementation. @@ -255,7 +262,7 @@ Add a listener for any context that is broadcast on the channel: const listener = await channel.addContextListener(null, context => { if (context.type === 'fdc3.contact') { // handle the contact - } else if (context.type === 'fdc3.instrument') => { + } else if (context.type === 'fdc3.instrument') { // handle the instrument } }); @@ -306,6 +313,36 @@ if listenerResult.Value != nil { +Add a listener for multiple specific context types: + + + + +```ts +// Listen for multiple specific context types +const multiListener = await channel.addContextListener( + ['fdc3.instrument', 'fdc3.contact', 'fdc3.portfolio'], + (context, metadata) => { + console.log(`Received ${context.type} from ${metadata?.source}`); + } +); + +// Listen for specific types plus all others (null in array - treated as if null was passed) +const combinedListener = await channel.addContextListener( + ['fdc3.instrument', null], + (context, metadata) => { + // Handles ALL context types (because null is in the array) + } +); + +// later +multiListener.unsubscribe(); +combinedListener.unsubscribe(); +``` + + + + Adding listeners for specific types of context that is broadcast on the channel: diff --git a/website/docs/api/ref/DesktopAgent.md b/website/docs/api/ref/DesktopAgent.md index 0f0d0ba18..c3408732d 100644 --- a/website/docs/api/ref/DesktopAgent.md +++ b/website/docs/api/ref/DesktopAgent.md @@ -162,7 +162,11 @@ type IDesktopAgent interface { ```ts +// Single context type addContextListener(contextType: string | null, handler: ContextHandler): Promise; + +// Array of context types +addContextListener(contextTypes: (string | null)[], handler: ContextHandler): Promise; ``` @@ -186,6 +190,8 @@ func (desktopAgent *DesktopAgent) AddContextListener(contextType string, handler Adds a listener for incoming context broadcasts from the Desktop Agent (via a User channel or [`fdc3.open`](#open) API call). If the consumer is only interested in a context of a particular type, they can specify that type. If the consumer is able to receive context of any type or will inspect types received, then they can pass `null` as the `contextType` parameter to receive all context types. +Alternatively, you can pass an array of context types to listen for multiple specific types at once. If the array contains `null`, it will be treated as if `null` was passed directly (meaning listen to all context types), so any other context types are ignored as the listener will receive all context types. Empty arrays will be ignored. + Context broadcasts are primarily received from apps that are joined to the same User Channel as the listening application, hence, if the application is not currently joined to a User Channel no broadcasts will be received from User channels. If this function is called after the app has already joined a channel and the channel already contains context that matches the type of the context listener, then it will be called immediately and the context passed to the handler function. If `null` was passed as the context type for the listener and the channel contains context, then the handler function will be called immediately with the most recent context - regardless of type. Context may also be received via this listener if the application was launched via a call to [`fdc3.open`](#open), where context was passed as an argument. In order to receive this, applications SHOULD add their context listener as quickly as possible after launch, or an error MAY be returned to the caller and the context may not be delivered. The exact timeout used is set by the Desktop Agent implementation, but MUST be at least 15 seconds. @@ -211,6 +217,22 @@ const contactListener = await fdc3.addContextListener('fdc3.contact', (contact, console.log(`Received context message\nContext: ${contact}\nOriginating app: ${metadata?.source}`); //do something else with the context }); + +// Listen for multiple specific context types +const multiListener = await fdc3.addContextListener( + ['fdc3.instrument', 'fdc3.contact', 'fdc3.portfolio'], + (context, metadata) => { + console.log(`Received ${context.type} from ${metadata?.source}`); + } +); + +// Listen for specific types plus all others (null in array - treated as if null was passed) +const combinedListener = await fdc3.addContextListener( + ['fdc3.instrument', null], + (context, metadata) => { + // Handles ALL context types (because null is in the array) + } +); ``` From 0590d0cad58ee77b563cbe79ac2dc6a4bc5a692d Mon Sep 17 00:00:00 2001 From: kemerava Date: Fri, 27 Mar 2026 14:39:32 -0400 Subject: [PATCH 2/3] Updating AgentProxy --- .../fdc3-agent-proxy/src/DesktopAgentProxy.ts | 17 +++- .../src/channels/ChannelSupport.ts | 2 +- .../src/channels/DefaultChannel.ts | 27 ++++++- .../src/channels/DefaultChannelSupport.ts | 65 +++++++++++++-- .../src/listeners/DefaultContextListener.ts | 80 +++++++++++++++---- .../fdc3-schema/generated/api/BrowserTypes.ts | 11 ++- 6 files changed, 169 insertions(+), 33 deletions(-) diff --git a/packages/fdc3-agent-proxy/src/DesktopAgentProxy.ts b/packages/fdc3-agent-proxy/src/DesktopAgentProxy.ts index d2669e7a6..d299fbcb4 100644 --- a/packages/fdc3-agent-proxy/src/DesktopAgentProxy.ts +++ b/packages/fdc3-agent-proxy/src/DesktopAgentProxy.ts @@ -2,6 +2,7 @@ import { AppIdentifier, AppMetadata, ContextHandler, + ContextType, DesktopAgent, EventHandler, FDC3EventTypes, @@ -92,10 +93,10 @@ export class DesktopAgentProxy implements DesktopAgent, Connectable { } addContextListener( - contextTypeOrHandler: ContextHandler | string | null, + contextTypeOrHandler: ContextHandler | ContextType | null | (ContextType | null)[], handler?: ContextHandler ): Promise { - let theContextType: string | null; + let theContextType: ContextType | null | (ContextType | null)[]; let theHandler: ContextHandler; if (contextTypeOrHandler == null && typeof handler === 'function') { @@ -104,6 +105,16 @@ export class DesktopAgentProxy implements DesktopAgent, Connectable { } else if (typeof contextTypeOrHandler === 'string' && typeof handler === 'function') { theContextType = contextTypeOrHandler; theHandler = handler; + } else if (Array.isArray(contextTypeOrHandler) && typeof handler === 'function') { + // Handle array-based context types + if (contextTypeOrHandler.length === 0) { + // Empty array + return Promise.resolve({ + unsubscribe: () => Promise.resolve(), + }); + } + theContextType = contextTypeOrHandler; // Pass the array directly + theHandler = handler; } else if (typeof contextTypeOrHandler === 'function') { // deprecated one-arg version theContextType = null; @@ -114,7 +125,7 @@ export class DesktopAgentProxy implements DesktopAgent, Connectable { throw new Error('Invalid arguments passed to addContextListener!'); } - return this.channels.addContextListener(theHandler, theContextType); + return this.channels.addContextListener(theHandler, theContextType as string | null | (string | null)[]); } getUserChannels() { diff --git a/packages/fdc3-agent-proxy/src/channels/ChannelSupport.ts b/packages/fdc3-agent-proxy/src/channels/ChannelSupport.ts index fbdfb7121..04c1e819a 100644 --- a/packages/fdc3-agent-proxy/src/channels/ChannelSupport.ts +++ b/packages/fdc3-agent-proxy/src/channels/ChannelSupport.ts @@ -7,7 +7,7 @@ export interface ChannelSupport { createPrivateChannel(): Promise; leaveUserChannel(): Promise; joinUserChannel(id: string): Promise; - addContextListener(handler: ContextHandler, type: string | null): Promise; + addContextListener(handler: ContextHandler, type: string | null | (string | null)[]): Promise; /** * TODO: Move handling for userChannelChanged out of ChannelSupport and update type to filter to channel events only. diff --git a/packages/fdc3-agent-proxy/src/channels/DefaultChannel.ts b/packages/fdc3-agent-proxy/src/channels/DefaultChannel.ts index ce5cc1028..198c74a62 100644 --- a/packages/fdc3-agent-proxy/src/channels/DefaultChannel.ts +++ b/packages/fdc3-agent-proxy/src/channels/DefaultChannel.ts @@ -71,10 +71,10 @@ export class DefaultChannel implements Channel { } async addContextListener( - contextTypeOrHandler: string | null | ContextHandler, + contextTypeOrHandler: string | null | ContextHandler | (string | null)[], handler?: ContextHandler ): Promise { - let theContextType: string | null; + let theContextType: string | null | (string | null)[]; let theHandler: ContextHandler; if (contextTypeOrHandler == null && typeof handler === 'function') { @@ -83,6 +83,16 @@ export class DefaultChannel implements Channel { } else if (typeof contextTypeOrHandler === 'string' && typeof handler === 'function') { theContextType = contextTypeOrHandler; theHandler = handler; + } else if (Array.isArray(contextTypeOrHandler) && typeof handler === 'function') { + // Handle array-based context types + if (contextTypeOrHandler.length === 0) { + // Empty array + return { + unsubscribe: () => Promise.resolve(), + }; + } + theContextType = contextTypeOrHandler; // Pass the array directly + theHandler = handler; } else if (typeof contextTypeOrHandler === 'function') { // deprecated one-arg version theContextType = null; @@ -93,10 +103,19 @@ export class DefaultChannel implements Channel { throw new Error('Invalid arguments passed to addContextListener!'); } - return await this.addContextListenerInner(theContextType, theHandler); + // Handle array case by creating individual listeners for each type + if (Array.isArray(theContextType)) { + // Pass the array directly to DefaultContextListener which will handle multiple listeners + return await this.addContextListenerInner(theContextType, theHandler); + } + + return await this.addContextListenerInner(theContextType as string | null, theHandler); } - async addContextListenerInner(contextType: string | null, theHandler: ContextHandler): Promise { + async addContextListenerInner( + contextType: string | null | (string | null)[], + theHandler: ContextHandler + ): Promise { const listener = new DefaultContextListener( this.messaging, this.messageExchangeTimeout, diff --git a/packages/fdc3-agent-proxy/src/channels/DefaultChannelSupport.ts b/packages/fdc3-agent-proxy/src/channels/DefaultChannelSupport.ts index b370d6f06..6dd36dc76 100644 --- a/packages/fdc3-agent-proxy/src/channels/DefaultChannelSupport.ts +++ b/packages/fdc3-agent-proxy/src/channels/DefaultChannelSupport.ts @@ -251,7 +251,28 @@ export class DefaultChannelSupport implements ChannelSupport { } } - async addContextListener(handler: ContextHandler, type: string | null): Promise { + async addContextListener(handler: ContextHandler, type: string | null | (string | null)[]): Promise { + // Handle array-based context types + if (Array.isArray(type)) { + if (type.length === 0) { + // Empty array - return a dummy listener that does nothing + return Promise.resolve({ + unsubscribe: () => Promise.resolve(), + id: 'dummy-listener', + }); + } + + // If array contains null, treat as null (listen to all contexts) + if (type.includes(null)) { + return this.addContextListener(handler, null); + } + + // For multiple specific types, pass the array directly to DefaultContextListener + // The DefaultContextListener will handle creating multiple individual listeners internally + return this.addContextListener(handler, type); + } + + // Handle single context type /** * Utility class used to wrap the DefaultContextListener to match the internal channel id * and ensure it gets removed when its unsubscribe function is called. @@ -263,7 +284,7 @@ export class DefaultChannelSupport implements ChannelSupport { container: DefaultChannelSupport, messaging: Messaging, messageExchangeTimeout: number, - contextType: string | null, + contextType: string | null | (string | null)[], handler: ContextHandler, messageType: string = 'broadcastEvent' ) { @@ -283,7 +304,23 @@ export class DefaultChannelSupport implements ChannelSupport { async changeChannel(): Promise { if (this.container.currentChannel != null) { - const context = await this.container.currentChannel?.getCurrentContext(this.contextType ?? undefined); + // Handle array context types for getCurrentContext + let contextTypeParam: string | undefined; + if (Array.isArray(this.contextType)) { + // For arrays, if it contains null, don't pass a type parameter (get all contexts) + // Otherwise, get the current context without filtering by type to ensure we don't miss any + // The individual listeners will filter their specific types + if (this.contextType.includes(null)) { + contextTypeParam = undefined; + } else { + // Don't filter by type - let the individual listeners handle their own filtering + contextTypeParam = undefined; + } + } else if (this.contextType != null) { + contextTypeParam = this.contextType; + } + + const context = await this.container.currentChannel?.getCurrentContext(contextTypeParam); if (context) { this.handler(context); } @@ -299,12 +336,28 @@ export class DefaultChannelSupport implements ChannelSupport { } filter(m: BroadcastEvent): boolean { + // Handle array context types in filtering + let contextTypeMatch = false; + if (Array.isArray(this.contextType)) { + // For arrays, match if any type in the array matches or if null is included (match all) + if (this.contextType.includes(null)) { + contextTypeMatch = true; // null means match all context types + } else { + contextTypeMatch = this.contextType.includes(m.payload.context?.type ?? null); + } + } else { + // Single context type - use original logic + contextTypeMatch = m.payload.context?.type == this.contextType || this.contextType == null; + } + return ( - m.type == this.messageType && - (this.onAMatchingChannel(m) || this.openBroadcastEvent(m)) && - (m.payload.context?.type == this.contextType || this.contextType == null) + m.type == this.messageType && (this.onAMatchingChannel(m) || this.openBroadcastEvent(m)) && contextTypeMatch ); } + + action(m: BroadcastEvent): void { + this.handler(m.payload.context); + } } const listener = new UnsubscribingDefaultContextListener( diff --git a/packages/fdc3-agent-proxy/src/listeners/DefaultContextListener.ts b/packages/fdc3-agent-proxy/src/listeners/DefaultContextListener.ts index 851fdddc2..3d05233e4 100644 --- a/packages/fdc3-agent-proxy/src/listeners/DefaultContextListener.ts +++ b/packages/fdc3-agent-proxy/src/listeners/DefaultContextListener.ts @@ -10,37 +10,83 @@ export class DefaultContextListener { private readonly channelId: string | null; protected readonly messageType: string; - protected readonly contextType: string | null; + protected readonly contextType: string | null | (string | null)[]; constructor( messaging: Messaging, messageExchangeTimeout: number, channelId: string | null, - contextType: string | null, + contextType: string | null | (string | null)[], handler: ContextHandler, messageType: string = 'broadcastEvent' ) { - super( - messaging, - messageExchangeTimeout, - { channelId, contextType }, - handler, - 'addContextListenerRequest', - 'addContextListenerResponse', - 'contextListenerUnsubscribeRequest', - 'contextListenerUnsubscribeResponse' - ); + // For arrays, use the contextTypes field in the payload + if (Array.isArray(contextType)) { + // Handle empty array case - will be caught upstream and return dummy listener + if (contextType.length === 0) { + throw new Error('Empty arrays should be handled upstream'); + } + + // If array contains null, create a single listener for all contexts + if (contextType.includes(null)) { + super( + messaging, + messageExchangeTimeout, + { channelId, contextType: null }, + handler, + 'addContextListenerRequest', + 'addContextListenerResponse', + 'contextListenerUnsubscribeRequest', + 'contextListenerUnsubscribeResponse' + ); + } else { + // Use the contextTypes field for array-based context types + super( + messaging, + messageExchangeTimeout, + { channelId, contextType: undefined, contextTypes: contextType }, + handler, + 'addContextListenerRequest', + 'addContextListenerResponse', + 'contextListenerUnsubscribeRequest', + 'contextListenerUnsubscribeResponse' + ); + } + } else { + // Single context type - use original logic + super( + messaging, + messageExchangeTimeout, + { channelId, contextType }, + handler, + 'addContextListenerRequest', + 'addContextListenerResponse', + 'contextListenerUnsubscribeRequest', + 'contextListenerUnsubscribeResponse' + ); + } + this.channelId = channelId; this.messageType = messageType; this.contextType = contextType; } filter(m: BroadcastEvent): boolean { - return ( - m.type == this.messageType && - m.payload.channelId == this.channelId && - (m.payload.context?.type == this.contextType || this.contextType == null) - ); + // Handle array context types in filtering + let contextTypeMatch = false; + if (Array.isArray(this.contextType)) { + // For arrays, match if any type in the array matches or if null is included (match all) + if (this.contextType.includes(null)) { + contextTypeMatch = true; // null means match all context types + } else { + contextTypeMatch = this.contextType.includes(m.payload.context?.type ?? null); + } + } else { + // Single context type - use original logic + contextTypeMatch = m.payload.context?.type == this.contextType || this.contextType == null; + } + + return m.type == this.messageType && m.payload.channelId == this.channelId && contextTypeMatch; } action(m: BroadcastEvent): void { diff --git a/packages/fdc3-schema/generated/api/BrowserTypes.ts b/packages/fdc3-schema/generated/api/BrowserTypes.ts index cbb4e3d2b..0c78bcc63 100644 --- a/packages/fdc3-schema/generated/api/BrowserTypes.ts +++ b/packages/fdc3-schema/generated/api/BrowserTypes.ts @@ -731,7 +731,13 @@ export interface AddContextListenerRequestPayload { * The type of context to listen for OR `null` indicating that it should listen to all * context types. */ - contextType: null | string; + contextType?: null | string; + /** + * Array of context types to listen for. May contain `null` to listen to all context types + * in addition to specific types. When `null` is present, other context types are ignored as + * the listener will receive all context types. + */ + contextTypes?: Array; } /** @@ -5116,7 +5122,8 @@ const typeMap: any = { AddContextListenerRequestPayload: o( [ { json: 'channelId', js: 'channelId', typ: u(null, '') }, - { json: 'contextType', js: 'contextType', typ: u(null, '') }, + { json: 'contextType', js: 'contextType', typ: u(undefined, u(null, '')) }, + { json: 'contextTypes', js: 'contextTypes', typ: u(undefined, a(u(null, ''))) }, ], false ), From 7ca9ccf44ed385ed83b28a732ed4a5c595113071 Mon Sep 17 00:00:00 2001 From: kemerava Date: Mon, 30 Mar 2026 10:27:38 -0400 Subject: [PATCH 3/3] Allowing null for empty context type --- .../fdc3-for-web/fdc3-web-impl/src/handlers/BroadcastHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/BroadcastHandler.ts b/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/BroadcastHandler.ts index 8de807c5c..99cdda77d 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/BroadcastHandler.ts +++ b/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/BroadcastHandler.ts @@ -400,7 +400,7 @@ export class BroadcastHandler implements MessageHandler { instanceId: from.instanceId ?? 'no-instance-id', channelId: channelId, listenerUuid: sc.createUUID(), - contextType: arg0.payload.contextType, + contextType: arg0.payload.contextType ?? null, }; this.contextListeners.push(lr);