Skip to content

Commit 42e8436

Browse files
authored
Implement ISelectApplicationStrategy (#276)
* define ISelectApplicationStrategy interface * refactor app resolver implementations to delegate opening apps to desktop agent * implement select app functionality * add comments * update changelog * update readme * pr feedback
1 parent 5280889 commit 42e8436

17 files changed

+664
-224
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ open(params: OpenApplicationStrategyResolverParams): Promise<string>;
6161
}
6262
}
6363
```
64+
* `applicationStrategies` parameter in `RootDesktopAgentFactoryParams` now accepts `(IOpenApplicationStrategy | ISelectApplicationStrategy)[]` instead of just `IOpenApplicationStrategy[]`
65+
* `ISelectApplicationStrategy` - new interface for selecting/focusing existing application instances. This allows strategies to restore minimised windows or bring windows to the front. Define strategies with `canSelectApp()` and `selectApp()` methods:
66+
67+
```ts
68+
export interface ISelectApplicationStrategy {
69+
manifestKey?: string;
70+
canSelectApp(params: SelectApplicationStrategyParams): Promise<boolean>;
71+
selectApp(params: SelectApplicationStrategyParams): Promise<void>;
72+
}
73+
```
74+
75+
### Changed
76+
77+
* IAppResolver implementations no longer have to return an `FullyQualifiedAppIdentifier`. Instead they can just return an `AppIdentifier`. If a non-qualified identifier is returned the Desktop Agent will handle the responsibility of opening a new instance of that app.
6478

6579
## 0.9.2 (2025-12-01)
6680

README.md

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const agent = await getAgent({
3131
new DesktopAgentFactory().createRoot({
3232
uiProvider: agent => Promise.resolve(new AppResolverComponent(agent, document)),
3333
appDirectoryEntries: ['http://localhost:4299/v2/apps'],
34-
openStrategies: [{
34+
applicationStrategies: [{
3535
canOpen: (params: OpenApplicationStrategyParams, context?: Context) => { /* define whether an app should open */ },
3636
open: (params: OpenApplicationStrategyParams, context?: Context) => { /* define how an app should open */ }
3737
}],
@@ -118,6 +118,136 @@ const agent = await getAgent({
118118

119119
For more advanced usage, see the [test-harness](./projects/test-harness/README.md) example app.
120120

121+
#### Local App Directories with Live Updates
122+
123+
Local app directories can receive live updates via an async iterator. This is useful for dynamically adding or updating app definitions at runtime:
124+
125+
```ts
126+
const updates: AsyncIterator<AppDirectoryApplication | AppDirectoryApplication[]>;
127+
128+
const agent = await getAgent({
129+
failover: () =>
130+
new DesktopAgentFactory().createRoot({
131+
appDirectoryEntries: [
132+
{
133+
host: 'my-domain.com',
134+
apps: [
135+
{ appId: 'static-app', title: 'Static App', type: 'web', details: { url: 'https://example.com/static' } }
136+
],
137+
updates,
138+
}
139+
],
140+
}),
141+
});
142+
```
143+
144+
### Singleton Apps
145+
146+
Apps can be configured as singletons to prevent multiple instances from being opened. When the intent resolver UI is displayed, singleton apps with an active instance will not appear in the "Open New" section. Users can only select the existing instance.
147+
148+
Configure singleton behavior via the `hostManifests` property in your app directory entry:
149+
150+
```ts
151+
{
152+
appId: 'my-singleton-app',
153+
title: 'My Singleton App',
154+
type: 'web',
155+
details: { url: 'https://example.com/singleton' },
156+
hostManifests: {
157+
'MorganStanley.fdc3-web': { singleton: true }
158+
}
159+
}
160+
```
161+
162+
## Custom Application Strategies
163+
164+
Application strategies control how apps are opened and selected. There are two types of strategies:
165+
166+
### Open Application Strategy
167+
168+
Defines how new app instances are launched. Implement `IOpenApplicationStrategy`:
169+
170+
```js
171+
import { subscribeToConnectionAttemptUuids } from "@morgan-stanley/fdc3-web";
172+
173+
const customOpenStrategy = {
174+
manifestKey: 'MyCustomManifest', // Optional: key to extract from hostManifests
175+
176+
canOpen: async (params) => {
177+
// Return true if this strategy can open the app
178+
return params.appDirectoryRecord.type === 'web';
179+
},
180+
181+
open: async (params) => {
182+
const newWindow = window.open(params.appDirectoryRecord.details.url);
183+
// return connectionAttemptUUID received from new window
184+
return new Promise(resolve => {
185+
const subscriber = subscribeToConnectionAttemptUuids(
186+
window, // the current window
187+
newWindow,
188+
connectionAttemptUuid => {
189+
subscriber.unsubscribe();
190+
191+
resolve(connectionAttemptUuid);
192+
},
193+
);
194+
});
195+
}
196+
};
197+
```
198+
199+
### Select Application Strategy
200+
201+
Defines how existing app instances are focused or brought to the foreground. Implement `ISelectApplicationStrategy`:
202+
203+
```js
204+
const customSelectStrategy = {
205+
manifestKey: 'MyCustomManifest', // Optional: key to extract from hostManifests
206+
207+
canSelectApp: async (params) => {
208+
// Return true if this strategy can select/focus the app
209+
return true;
210+
},
211+
212+
selectApp: async (params) => {
213+
// Focus or bring the existing app instance to the foreground
214+
// params.appIdentifier contains the instanceId of the target app
215+
}
216+
};
217+
```
218+
219+
Pass strategies when creating the root agent:
220+
221+
```js
222+
const agent = await getAgent({
223+
failover: () =>
224+
new DesktopAgentFactory().createRoot({
225+
applicationStrategies: [customOpenStrategy, customSelectStrategy],
226+
}),
227+
});
228+
```
229+
230+
Strategies are evaluated in order. The first strategy where `canOpen()` or `canSelectApp()` returns `true` will be used.
231+
232+
## Backoff Retry for App Directory Loading
233+
234+
When loading remote app directories, the agent can retry failed requests with exponential backoff:
235+
236+
```js
237+
const agent = await getAgent({
238+
failover: () =>
239+
new DesktopAgentFactory().createRoot({
240+
appDirectoryEntries: ['https://my-app-directory.com/v2/apps'],
241+
backoffRetry: {
242+
maxAttempts: 5, // Maximum number of retry attempts (default: 3)
243+
baseDelay: 500 // Initial delay in ms, doubles with each retry (default: 250)
244+
}
245+
}),
246+
});
247+
```
248+
249+
With `baseDelay: 500` and `maxAttempts: 5`, retries would occur at approximately 500ms, 1000ms, 2000ms, and 4000ms intervals.
250+
121251
### Controlling Logging Levels
122252

123253
The `getAgent` function accepts a `logLevels` parameter that allows fine-grained control over logging behavior:

projects/fdc3-web/src/agent/desktop-agent.factory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export class DesktopAgentFactory {
8282
rootMessagePublisher,
8383
directory: directory,
8484
channelFactory: new ChannelFactory(),
85-
openStrategies: factoryParams.openStrategies,
85+
applicationStrategies: factoryParams.applicationStrategies,
8686
logLevels: factoryParams.logLevels,
8787
});
8888

0 commit comments

Comments
 (0)