Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 14 additions & 3 deletions packages/fdc3-agent-proxy/src/DesktopAgentProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
AppIdentifier,
AppMetadata,
ContextHandler,
ContextType,
DesktopAgent,
EventHandler,
FDC3EventTypes,
Expand Down Expand Up @@ -92,10 +93,10 @@ export class DesktopAgentProxy implements DesktopAgent, Connectable {
}

addContextListener(
contextTypeOrHandler: ContextHandler | string | null,
contextTypeOrHandler: ContextHandler | ContextType | null | (ContextType | null)[],
handler?: ContextHandler
): Promise<Listener> {
let theContextType: string | null;
let theContextType: ContextType | null | (ContextType | null)[];
let theHandler: ContextHandler;

if (contextTypeOrHandler == null && typeof handler === 'function') {
Expand All @@ -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;
Expand All @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion packages/fdc3-agent-proxy/src/channels/ChannelSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface ChannelSupport {
createPrivateChannel(): Promise<PrivateChannel>;
leaveUserChannel(): Promise<void>;
joinUserChannel(id: string): Promise<void>;
addContextListener(handler: ContextHandler, type: string | null): Promise<Listener>;
addContextListener(handler: ContextHandler, type: string | null | (string | null)[]): Promise<Listener>;

/**
* TODO: Move handling for userChannelChanged out of ChannelSupport and update type to filter to channel events only.
Expand Down
27 changes: 23 additions & 4 deletions packages/fdc3-agent-proxy/src/channels/DefaultChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ export class DefaultChannel implements Channel {
}

async addContextListener(
contextTypeOrHandler: string | null | ContextHandler,
contextTypeOrHandler: string | null | ContextHandler | (string | null)[],
handler?: ContextHandler
): Promise<Listener> {
let theContextType: string | null;
let theContextType: string | null | (string | null)[];
let theHandler: ContextHandler;

if (contextTypeOrHandler == null && typeof handler === 'function') {
Expand All @@ -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;
Expand All @@ -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<Listener> {
async addContextListenerInner(
contextType: string | null | (string | null)[],
theHandler: ContextHandler
): Promise<Listener> {
const listener = new DefaultContextListener(
this.messaging,
this.messageExchangeTimeout,
Expand Down
65 changes: 59 additions & 6 deletions packages/fdc3-agent-proxy/src/channels/DefaultChannelSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,28 @@ export class DefaultChannelSupport implements ChannelSupport {
}
}

async addContextListener(handler: ContextHandler, type: string | null): Promise<Listener> {
async addContextListener(handler: ContextHandler, type: string | null | (string | null)[]): Promise<Listener> {
// 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.
Expand All @@ -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'
) {
Expand All @@ -283,7 +304,23 @@ export class DefaultChannelSupport implements ChannelSupport {

async changeChannel(): Promise<void> {
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);
}
Expand All @@ -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(
Expand Down
80 changes: 63 additions & 17 deletions packages/fdc3-agent-proxy/src/listeners/DefaultContextListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 9 additions & 2 deletions packages/fdc3-schema/generated/api/BrowserTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<null | string>;
}

/**
Expand Down Expand Up @@ -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
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
]
}
}
Expand Down
11 changes: 11 additions & 0 deletions packages/fdc3-standard/src/api/Channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@
*/
addContextListener(contextType: string | null, handler: ContextHandler): Promise<Listener>;

/**
* 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<Listener>;

/**
* 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.
*
Expand Down Expand Up @@ -112,5 +123,5 @@
/**
* @deprecated use `addContextListener(null, handler)` instead of `addContextListener(handler)`.
*/
addContextListener(handler: ContextHandler): Promise<Listener>;

Check warning on line 126 in packages/fdc3-standard/src/api/Channel.ts

View workflow job for this annotation

GitHub Actions / test

All addContextListener signatures should be adjacent
}
Loading
Loading