Skip to content

Commit 6acc383

Browse files
committed
Add getAgent opt-in flags for own broadcasts and self intent resolution
Implements #1751 and #1860 by adding two optional booleans to GetAgentParams: - receiveOwnBroadcasts: when set, the Desktop Agent delivers an app's own broadcasts back to it. Defaults to false (current behaviour). - resolveOwnIntents: when set, the raising app's own instance is considered when resolving an intent it raised. Defaults to false (current behaviour); other instances of the same app remain eligible either way. The flags travel on the WCP4ValidateAppIdentity handshake message and are stored per-instance by the reference Desktop Agent (each instance of an app may connect with different getAgent options). BroadcastHandler makes its self-delivery filter conditional on receiveOwnBroadcasts; IntentHandler excludes the raising instance (by instanceId) unless resolveOwnIntents is set, dropping any AppIntent left with no resolvers so a self-only intent yields NoAppsFound. Updates the API spec, getAgent, broadcast, Channel and raiseIntent/ raiseIntentForContext documentation (including net-new intent self-resolution wording), regenerates schema types, and adds conformance/BDD tests.
1 parent 13a47ff commit 6acc383

19 files changed

Lines changed: 282 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1212
* Added conformance coverage for `ChannelError.NoChannelFound`, `ChannelError.MalformedContext`, and `ChannelError.InvalidArguments`. ([#1779](https://github.com/finos/FDC3/issues/1779))
1313
* Added conformance coverage verifying that Desktop Agent methods continue to work when destructured from the `fdc3` object. ([#1778](https://github.com/finos/FDC3/issues/1778))
1414
* Added standalone Workbench examples for the FDC3 2.2 `fdc3.action`, `fdc3.fileAttachment`, `fdc3.message`, `fdc3.orderList`, `fdc3.tradeList`, and `fdc3.timeRange` context types. ([#1949](https://github.com/finos/FDC3/pull/1949))
15+
* Added `receiveOwnBroadcasts` and `resolveOwnIntents` options to `getAgent`, allowing an application to opt in to receiving its own broadcasts and/or having its own instance considered when resolving intents it raises. Both default to the existing behaviour (an app does not receive its own broadcasts and an intent is not resolved to the raising instance; other instances of the same app remain eligible). The options are carried on the `WCP4ValidateAppIdentity` handshake message and stored per-instance by the Desktop Agent. Updated the API spec, `getAgent`, `broadcast`, `Channel` and `raiseIntent`/`raiseIntentForContext` documentation, and added conformance tests. ([#1751](https://github.com/finos/FDC3/issues/1751), [#1860](https://github.com/finos/FDC3/issues/1860))
1516
* Added advanced conformance tests (`fdc3.intentListenerConflict`) covering intent listener conflicts, verifying that `addIntentListener`/`addIntentListenerWithContext` reject with `ResolveError.IntentListenerConflict` for conflicting listeners (unfiltered, or overlapping context types) and allow non-overlapping filtered listeners, listeners for different intents, and re-adding after `unsubscribe()`. Added the corresponding test definitions to the "Avoiding Adding Multiple Intent Listeners" section of the Intents conformance docs.
1617
* Added a classification field to Instrument context type ([#1665](https://github.com/finos/FDC3/pull/1665))
1718
* Added Go language binding. ([#1483](https://github.com/finos/FDC3/pull/1483))

packages/fdc3-get-agent/src/strategies/IdentityValidationHandler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ export class IdentityValidationHandler {
5656
payload: {
5757
identityUrl,
5858
actualUrl,
59+
...(this.options.receiveOwnBroadcasts !== undefined && {
60+
receiveOwnBroadcasts: this.options.receiveOwnBroadcasts,
61+
}),
62+
...(this.options.resolveOwnIntents !== undefined && {
63+
resolveOwnIntents: this.options.resolveOwnIntents,
64+
}),
5965
},
6066
};
6167

packages/fdc3-get-agent/src/strategies/getAgent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ export const getAgent: GetAgentType = (params?: GetAgentParams) => {
211211
channelSelector: true,
212212
intentResolver: true,
213213
timeoutMs: DEFAULT_GETAGENT_TIMEOUT_MS,
214+
receiveOwnBroadcasts: false,
215+
resolveOwnIntents: false,
214216
//default log levels are set in the relevant logging utils
215217
};
216218

packages/fdc3-schema/generated/api/BrowserTypes.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,20 @@ export interface WebConnectionProtocol4ValidateAppIdentityPayload {
299299
* Instance UUID associated with the requested instanceId.
300300
*/
301301
instanceUuid?: string;
302+
/**
303+
* Flag indicating that the application wishes to receive its own broadcasts, i.e. context
304+
* messages it broadcasts to a channel it is joined to (or listening on) should be delivered
305+
* back to it. Defaults to false when omitted, in which case the Desktop Agent SHOULD NOT
306+
* deliver the application's own broadcasts back to it.
307+
*/
308+
receiveOwnBroadcasts?: boolean;
309+
/**
310+
* Flag indicating that the application is willing for its own instance to be considered
311+
* when resolving intents that it raises. Defaults to false when omitted, in which case the
312+
* Desktop Agent SHOULD NOT resolve an intent raised by the application instance to that
313+
* same instance (other instances of the same app remain eligible).
314+
*/
315+
resolveOwnIntents?: boolean;
302316
}
303317

304318
/**
@@ -5213,6 +5227,8 @@ const typeMap: any = {
52135227
{ json: 'identityUrl', js: 'identityUrl', typ: '' },
52145228
{ json: 'instanceId', js: 'instanceId', typ: u(undefined, '') },
52155229
{ json: 'instanceUuid', js: 'instanceUuid', typ: u(undefined, '') },
5230+
{ json: 'receiveOwnBroadcasts', js: 'receiveOwnBroadcasts', typ: u(undefined, true) },
5231+
{ json: 'resolveOwnIntents', js: 'resolveOwnIntents', typ: u(undefined, true) },
52165232
],
52175233
false
52185234
),

packages/fdc3-schema/schemas/api/WCP4ValidateAppIdentity.schema.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@
4545
"title": "instanceUuid",
4646
"description": "Instance UUID associated with the requested instanceId.",
4747
"type": "string"
48+
},
49+
"receiveOwnBroadcasts": {
50+
"title": "receiveOwnBroadcasts",
51+
"description": "Flag indicating that the application wishes to receive its own broadcasts, i.e. context messages it broadcasts to a channel it is joined to (or listening on) should be delivered back to it. Defaults to false when omitted, in which case the Desktop Agent SHOULD NOT deliver the application's own broadcasts back to it.",
52+
"type": "boolean"
53+
},
54+
"resolveOwnIntents": {
55+
"title": "resolveOwnIntents",
56+
"description": "Flag indicating that the application is willing for its own instance to be considered when resolving intents that it raises. Defaults to false when omitted, in which case the Desktop Agent SHOULD NOT resolve an intent raised by the application instance to that same instance (other instances of the same app remain eligible).",
57+
"type": "boolean"
4858
}
4959
},
5060
"additionalProperties": false,

packages/fdc3-standard/src/api/GetAgent.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,25 @@ export type GetAgentType = (params?: GetAgentParams) => Promise<DesktopAgent>;
8585
* @property {GetAgentLogLevels} logLevels Settings that determine what should
8686
* will logged by the getAgent() implementation and DesktopAgentProxy to the
8787
* JavaScript console.
88+
*
89+
* @property {boolean} receiveOwnBroadcasts Flag indicating that the application
90+
* wishes to receive its own broadcasts, i.e. context messages that it broadcasts
91+
* to a channel it is joined to (or listening on) should be delivered back to it.
92+
* Defaults to `false`, in which case a Desktop Agent SHOULD NOT deliver an
93+
* application's own broadcasts back to it. Applications can distinguish their own
94+
* broadcasts from those of other instances of the same app via the `source`
95+
* (`instanceId`) in the [`ContextMetadata`](ref/Metadata#contextmetadata) provided
96+
* to their context handlers. MAY be ignored by Desktop Agent Preload (container)
97+
* implementations.
98+
*
99+
* @property {boolean} resolveOwnIntents Flag indicating that the application is
100+
* willing for its own instance to be considered when resolving intents that it
101+
* raises (i.e. an intent it raises may be delivered back to the same instance).
102+
* Defaults to `false`, in which case a Desktop Agent SHOULD NOT resolve an intent
103+
* raised by an application instance to that same instance (other instances of the
104+
* same app remain eligible). If enabling this leaves no eligible targets, the
105+
* raising app receives a `NoAppsFound` error. MAY be ignored by Desktop Agent
106+
* Preload (container) implementations.
88107
*/
89108
export type GetAgentParams = {
90109
timeoutMs?: number;
@@ -94,6 +113,8 @@ export type GetAgentParams = {
94113
dontSetWindowFdc3?: boolean;
95114
failover?: (args: GetAgentParams) => Promise<WindowProxy | DesktopAgent>;
96115
logLevels?: GetAgentLogLevels;
116+
receiveOwnBroadcasts?: boolean;
117+
resolveOwnIntents?: boolean;
97118
};
98119

99120
/**

toolbox/fdc3-for-web/fdc3-web-impl/src/ServerContext.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ export type AppRegistration = {
1313
state: State;
1414
appId: string;
1515
instanceId: InstanceID;
16+
17+
/** Whether this instance opted in (via getAgent) to receiving its own
18+
* broadcasts. When falsy, the Desktop Agent does not deliver context
19+
* messages broadcast by this instance back to it. */
20+
receiveOwnBroadcasts?: boolean;
21+
22+
/** Whether this instance opted in (via getAgent) to having its own instance
23+
* considered when resolving intents that it raises. When falsy, the Desktop
24+
* Agent excludes this instance from resolution of intents it raises (other
25+
* instances of the same app remain eligible). */
26+
resolveOwnIntents?: boolean;
1627
};
1728

1829
/**

toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/BroadcastHandler.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,13 +445,17 @@ export class BroadcastHandler implements MessageHandler {
445445
return r.channelId == null && ucId == arg0.payload.channelId;
446446
};
447447

448+
// Unless the broadcasting app opted in (via getAgent's receiveOwnBroadcasts),
449+
// its own broadcasts are not delivered back to it.
450+
const receiveOwnBroadcasts = sc.getInstanceDetails(from.instanceId)?.receiveOwnBroadcasts ?? false;
451+
448452
const matchingListeners = this.contextListeners
449453
// Deliver the message to apps listening to the right channel
450454
.filter(r => matchesExactChannel(r) || matchesUserChannel(r))
451455
// Deliver the message to apps with matching context type listeners
452456
.filter(r => r.contextType == null || r.contextType == arg0.payload.context.type)
453-
// Don't deliver messages back to the broadcasting app
454-
.filter(r => r.instanceId !== from.instanceId);
457+
// Don't deliver messages back to the broadcasting app (unless it opted in)
458+
.filter(r => receiveOwnBroadcasts || r.instanceId !== from.instanceId);
455459

456460
const matchingApps: FullAppIdentifier[] = matchingListeners
457461
.map(r => {

toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/IntentHandler.ts

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,13 @@ export class IntentHandler implements MessageHandler {
426426

427427
async raiseIntentToAnyApp(arg0: IntentRequest[], sc: ServerContext<AppRegistration>): Promise<void> {
428428
const connectedApps = await sc.getConnectedApps();
429+
// Unless the raising app opted in (via getAgent's resolveOwnIntents), its own
430+
// instance is not considered when resolving an intent that it raised. Other
431+
// instances of the same app remain eligible.
432+
const raiser = arg0[0].from;
433+
const raiserResolvesOwnIntents = raiser.instanceId
434+
? (sc.getInstanceDetails(raiser.instanceId)?.resolveOwnIntents ?? false)
435+
: false;
429436
const matchingIntents = arg0.flatMap(i => this.directory.retrieveIntents(i.context.type, i.intent, undefined));
430437
const matchingRegistrations = arg0.flatMap(i =>
431438
this.registrations.filter(
@@ -439,29 +446,36 @@ export class IntentHandler implements MessageHandler {
439446

440447
const allIntents = this.directory.retrieveAllIntents();
441448

442-
const appIntents: AppIntent[] = uniqueIntentNames.map(i => {
443-
const directoryAppsWithIntent = matchingIntents.filter(mi => mi.intentName == i).map(mi => mi.appId);
444-
const runningDirectoryApps = connectedApps.filter(ca => directoryAppsWithIntent.includes(ca.appId));
445-
const appRegistrations = matchingRegistrations
446-
.filter(registration => registration.intentName === i) // filter registrations for the current intent
447-
.map(listener => ({ appId: listener.appId, instanceId: listener.instanceId, state: State.Connected }))
448-
.filter(appRegistration => allIntents.every(intent => intent.appId !== appRegistration.appId)); // filter out apps that have intents registered in the directory
449-
450-
const runningApps: AppRegistration[] = [...runningDirectoryApps, ...appRegistrations];
449+
const appIntents: AppIntent[] = uniqueIntentNames
450+
.map(i => {
451+
const directoryAppsWithIntent = matchingIntents.filter(mi => mi.intentName == i).map(mi => mi.appId);
452+
const runningDirectoryApps = connectedApps.filter(ca => directoryAppsWithIntent.includes(ca.appId));
453+
const appRegistrations = matchingRegistrations
454+
.filter(registration => registration.intentName === i) // filter registrations for the current intent
455+
.map(listener => ({ appId: listener.appId, instanceId: listener.instanceId, state: State.Connected }))
456+
.filter(appRegistration => allIntents.every(intent => intent.appId !== appRegistration.appId)); // filter out apps that have intents registered in the directory
457+
458+
const runningApps: AppRegistration[] = [...runningDirectoryApps, ...appRegistrations].filter(
459+
app => raiserResolvesOwnIntents || app.instanceId !== raiser.instanceId
460+
);
451461

452-
return {
453-
intent: {
454-
name: i,
455-
displayName: i,
456-
},
457-
apps: [
458-
...runningApps,
459-
...directoryAppsWithIntent.map(d => {
460-
return { appId: d };
461-
}),
462-
],
463-
};
464-
});
462+
return {
463+
intent: {
464+
name: i,
465+
displayName: i,
466+
},
467+
apps: [
468+
...runningApps,
469+
...directoryAppsWithIntent.map(d => {
470+
return { appId: d };
471+
}),
472+
],
473+
};
474+
// Drop any intent left with no resolvers (e.g. because the only handler was
475+
// the raising instance, which was excluded above), so the caller receives a
476+
// NoAppsFound error rather than an empty resolution.
477+
})
478+
.filter(appIntent => appIntent.apps.length > 0);
465479

466480
const narrowedAppIntents = await this.narrowIntents(arg0[0].from, appIntents, arg0[0].context, sc);
467481

toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/OpenHandler.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,13 @@ export class OpenHandler implements MessageHandler {
365365
sc.post(msg, instanceId);
366366
};
367367

368+
// Self-interaction preferences supplied by the app via getAgent, captured
369+
// per-instance (each instance of an app may connect with different options).
370+
const applySelfInteractionOptions = (registration: AppRegistration) => {
371+
registration.receiveOwnBroadcasts = arg0.payload.receiveOwnBroadcasts ?? false;
372+
registration.resolveOwnIntents = arg0.payload.resolveOwnIntents ?? false;
373+
};
374+
368375
if (arg0.payload.instanceUuid) {
369376
// existing app reconnecting
370377
console.debug('App attempting to reconnect:', arg0.payload.instanceUuid);
@@ -378,6 +385,7 @@ export class OpenHandler implements MessageHandler {
378385
', instanceId',
379386
arg0.payload.instanceUuid
380387
);
388+
applySelfInteractionOptions(appIdentity);
381389
sc.setInstanceDetails(from, appIdentity);
382390
sc.setAppState(from, State.Connected);
383391
return returnSuccess(appIdentity.appId, appIdentity.instanceId);
@@ -390,6 +398,8 @@ export class OpenHandler implements MessageHandler {
390398
// we need to assign an identity to this app - this should have been generated when it was launched
391399
const appIdentity = sc.getInstanceDetails(from);
392400
if (appIdentity) {
401+
applySelfInteractionOptions(appIdentity);
402+
sc.setInstanceDetails(from, appIdentity);
393403
sc.setAppState(appIdentity.instanceId, State.Connected);
394404
returnSuccess(appIdentity.appId, appIdentity.instanceId);
395405

0 commit comments

Comments
 (0)