@morgan-stanley/composeui-fdc3 0.1.0-alpha.11 → 0.1.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -135,7 +135,7 @@
135
135
  /**
136
136
  * @license
137
137
  * author: Morgan Stanley
138
- * composeui-messaging-abstractions.js v0.1.0-alpha.11
138
+ * composeui-messaging-abstractions.js v0.1.0-alpha.12
139
139
  * Released under the Apache-2.0 license.
140
140
  */
141
141
 
@@ -402,6 +402,12 @@
402
402
  static joinUserChannel() {
403
403
  return `${this.topicRoot}/${this.joinUserChannelSuffix}`;
404
404
  }
405
+ static channelSelectorFromUI(instanceId) {
406
+ return `${this.topicRoot}/channelSelector/UI/${instanceId}`;
407
+ }
408
+ static channelSelectorFromAPI(instanceId) {
409
+ return `${this.topicRoot}/channelSelector/API/${instanceId}`;
410
+ }
405
411
  static getInfo() {
406
412
  return `${this.topicRoot}/${this.getInfoSuffix}`;
407
413
  }
@@ -520,6 +526,7 @@
520
526
  }
521
527
  async subscribe(channelId, channelType) {
522
528
  await this.registerContextListener(channelId, channelType);
529
+ console.debug("Subscribed context listener with id: ", this.id, ", to channel: ", channelId, ", of type: ", channelType, ", for context type: ", this.contextType);
523
530
  const subscribeTopic = ComposeUITopic.broadcast(channelId, channelType);
524
531
  this.unsubscribable = await this.jsonMessaging.subscribeJson(subscribeTopic, async (context) => {
525
532
  if (!this.contextType || this.contextType == context.type) {
@@ -531,6 +538,7 @@
531
538
  }
532
539
  }
533
540
  });
541
+ console.log("Registered context listener with id: ", this.id, ", to topic: ", subscribeTopic);
534
542
  this.isSubscribed = true;
535
543
  }
536
544
  async handleContextMessage(context) {
@@ -562,15 +570,17 @@
562
570
  }
563
571
  async unsubscribe() {
564
572
  if (!this.unsubscribable || !this.isSubscribed) {
573
+ console.debug("The current listener is not subscribed.");
565
574
  return;
566
575
  }
567
576
  try {
568
577
  await this.leaveChannel();
578
+ console.debug("Unsubscribed context listener with id: ", this.id);
569
579
  }
570
580
  catch (err) {
571
581
  console.log(err);
572
582
  }
573
- this.unsubscribable.unsubscribe();
583
+ await this.unsubscribable.unsubscribe();
574
584
  this.isSubscribed = false;
575
585
  if (this.unsubscribeCallback) {
576
586
  this.unsubscribeCallback(this);
@@ -591,6 +601,7 @@
591
601
  this.id = response.id;
592
602
  }
593
603
  async leaveChannel() {
604
+ console.debug("Removing context listener with id: ", this.id);
594
605
  const request = new Fdc3RemoveContextListenerRequest(window.composeui.fdc3.config?.instanceId, this.id, this.contextType);
595
606
  const response = await this.jsonMessaging.invokeJsonService(ComposeUITopic.removeContextListener(), request);
596
607
  if (!response) {
@@ -652,7 +663,7 @@
652
663
  this.jsonMessaging = jsonMessaging;
653
664
  this.displayMetadata = displayMetadata;
654
665
  }
655
- //Broadcasting on the composeui/fdc3/v2.0/broadcast topic
666
+ //Broadcasting on the composeui/fdc3/v2.0/<channel>/broadcast topic
656
667
  async broadcast(context) {
657
668
  //Setting the last published context message.
658
669
  this.lastContexts.set(context.type, context);
@@ -1218,13 +1229,23 @@
1218
1229
  * and limitations under the License.
1219
1230
  *
1220
1231
  */
1221
- class MessagingChannelFactory {
1232
+ class MessagingChannelHandler {
1222
1233
  jsonMessaging;
1223
1234
  fdc3instanceId;
1235
+ channelSelector = undefined;
1224
1236
  constructor(jsonMessaging, fdc3instanceId) {
1225
1237
  this.jsonMessaging = jsonMessaging;
1226
1238
  this.fdc3instanceId = fdc3instanceId;
1227
1239
  }
1240
+ [Symbol.asyncDispose]() {
1241
+ return this.channelSelector
1242
+ ? this.channelSelector[Symbol.asyncDispose]()
1243
+ : Promise.resolve();
1244
+ }
1245
+ async configureChannelSelectorFromUI() {
1246
+ this.channelSelector = await this.jsonMessaging.registerService(ComposeUITopic.channelSelectorFromUI(this.fdc3instanceId), this.selectUserChannelFromUIHandler);
1247
+ console.debug("Configured channel selector for module: ", this.fdc3instanceId);
1248
+ }
1228
1249
  async getChannel(channelId, channelType) {
1229
1250
  const topic = ComposeUITopic.findChannel();
1230
1251
  const message = new Fdc3FindChannelRequest(channelId, channelType);
@@ -1283,20 +1304,37 @@
1283
1304
  return new ComposeUIChannel(channelId, "app", this.jsonMessaging);
1284
1305
  }
1285
1306
  async joinUserChannel(channelId) {
1286
- const topic = ComposeUITopic.joinUserChannel();
1287
- const request = new Fdc3JoinUserChannelRequest(channelId, this.fdc3instanceId);
1288
- const response = await this.jsonMessaging.invokeJsonService(topic, request);
1289
- if (!response) {
1290
- throw new Error(ChannelError.CreationFailed);
1307
+ try {
1308
+ const topic = ComposeUITopic.joinUserChannel();
1309
+ const request = new Fdc3JoinUserChannelRequest(channelId, this.fdc3instanceId);
1310
+ const response = await this.jsonMessaging.invokeJsonService(topic, request);
1311
+ console.debug("Received joinUserChannel response: ", response);
1312
+ if (!response) {
1313
+ throw new Error(ChannelError.CreationFailed);
1314
+ }
1315
+ if (response.error) {
1316
+ throw new Error(response.error);
1317
+ }
1318
+ if (!response.success) {
1319
+ throw new Error(ChannelError.CreationFailed);
1320
+ }
1321
+ var channel = new ComposeUIChannel(channelId, "user", this.jsonMessaging, response.displayMetadata);
1322
+ this.triggerChannelJoinedEvent(channel.id);
1323
+ return channel;
1291
1324
  }
1292
- if (response.error) {
1293
- throw new Error(response.error);
1325
+ catch (error) {
1326
+ console.error("Error joining user channel: ", error);
1327
+ throw error;
1294
1328
  }
1295
- if (!response.success) {
1296
- throw new Error(ChannelError.CreationFailed);
1329
+ }
1330
+ async triggerChannelJoinedEvent(id) {
1331
+ try {
1332
+ var result = await this.jsonMessaging.invokeService(ComposeUITopic.channelSelectorFromAPI(this.fdc3instanceId), id);
1333
+ console.debug("Triggered channel selector of container: ", this.fdc3instanceId, ", with: ", id, ", and got result:", result);
1334
+ }
1335
+ catch (error) {
1336
+ console.error("Error triggering channel joined event for module: ", this.fdc3instanceId, ", with channel id: ", id, ", error: ", error);
1297
1337
  }
1298
- var channel = new ComposeUIChannel(channelId, "user", this.jsonMessaging, response.displayMetadata);
1299
- return channel;
1300
1338
  }
1301
1339
  async getUserChannels() {
1302
1340
  var request = new Fdc3GetUserChannelsRequest(this.fdc3instanceId);
@@ -1343,6 +1381,31 @@
1343
1381
  const listener = new ComposeUIContextListener(openHandled, this.jsonMessaging, handler, contextType ?? undefined);
1344
1382
  return listener;
1345
1383
  }
1384
+ async leaveCurrentChannel() {
1385
+ await this.triggerChannelJoinedEvent(undefined);
1386
+ }
1387
+ async selectUserChannelFromUIHandler(request) {
1388
+ try {
1389
+ if (!request) {
1390
+ console.debug("Empty request received when the user channel selection was requested from UI.");
1391
+ return null;
1392
+ }
1393
+ var objectRequest = JSON.parse(request);
1394
+ var joinUserChannelRequest = objectRequest;
1395
+ console.debug("Parsed the request from the UI when user selected a user channel to join: ", joinUserChannelRequest);
1396
+ if (!joinUserChannelRequest || !joinUserChannelRequest.channelId) {
1397
+ console.debug("Invalid request received when user selected a user channel to join to from the UI: ", request);
1398
+ return null;
1399
+ }
1400
+ //We should join the channel requested by the user from the UI. -> this will trigger the handler of the module/container to show which channel the app is joined to.
1401
+ await window.fdc3.joinUserChannel(joinUserChannelRequest.channelId);
1402
+ return joinUserChannelRequest.channelId;
1403
+ }
1404
+ catch (error) {
1405
+ console.error("Error processing request when channel selector was invoked: ", request, ", error: ", error);
1406
+ return null;
1407
+ }
1408
+ }
1346
1409
  }
1347
1410
 
1348
1411
  /*
@@ -1372,16 +1435,16 @@
1372
1435
 
1373
1436
  class ComposeUIIntentResolution {
1374
1437
  jsonMessaging;
1375
- channelFactory;
1438
+ channelHandler;
1376
1439
  source;
1377
1440
  intent;
1378
1441
  messageId;
1379
- constructor(messageId, jsonMessaging, channelFactory, intent, source) {
1442
+ constructor(messageId, jsonMessaging, channelHandler, intent, source) {
1380
1443
  this.messageId = messageId;
1381
1444
  this.intent = intent;
1382
1445
  this.source = source;
1383
1446
  this.jsonMessaging = jsonMessaging;
1384
- this.channelFactory = channelFactory;
1447
+ this.channelHandler = channelHandler;
1385
1448
  }
1386
1449
  async getResult() {
1387
1450
  const intentResolutionRequest = new Fdc3GetIntentResultRequest(this.messageId, this.intent, this.source, this.source.version);
@@ -1393,7 +1456,7 @@
1393
1456
  throw new Error(result.error);
1394
1457
  }
1395
1458
  if (result.channelId && result.channelType) {
1396
- const channel = this.channelFactory.getChannel(result.channelId, result.channelType);
1459
+ const channel = this.channelHandler.getChannel(result.channelId, result.channelType);
1397
1460
  return channel;
1398
1461
  }
1399
1462
  else if (result.context) {
@@ -1508,13 +1571,13 @@
1508
1571
  *
1509
1572
  */
1510
1573
  class MessagingIntentsClient {
1511
- channelFactory;
1574
+ channelHandler;
1512
1575
  jsonMessaging;
1513
1576
  constructor(jsonMessaging, channelFactory) {
1514
1577
  if (!window.composeui.fdc3.config || !window.composeui.fdc3.config.instanceId) {
1515
1578
  throw new Error(ComposeUIErrors.InstanceIdNotFound);
1516
1579
  }
1517
- this.channelFactory = channelFactory;
1580
+ this.channelHandler = channelFactory;
1518
1581
  this.jsonMessaging = jsonMessaging;
1519
1582
  }
1520
1583
  async findIntent(intent, context, resultType) {
@@ -1542,7 +1605,7 @@
1542
1605
  return message.appIntents;
1543
1606
  }
1544
1607
  async getIntentResolution(messageId, intent, source) {
1545
- return new ComposeUIIntentResolution(messageId, this.jsonMessaging, this.channelFactory, intent, source);
1608
+ return new ComposeUIIntentResolution(messageId, this.jsonMessaging, this.channelHandler, intent, source);
1546
1609
  }
1547
1610
  async raiseIntent(intent, context, app) {
1548
1611
  if (typeof app == 'string') {
@@ -1557,7 +1620,7 @@
1557
1620
  if (response.error) {
1558
1621
  throw new Error(response.error);
1559
1622
  }
1560
- const intentResolution = new ComposeUIIntentResolution(response.messageId, this.jsonMessaging, this.channelFactory, response.intent, response.appMetadata);
1623
+ const intentResolution = new ComposeUIIntentResolution(response.messageId, this.jsonMessaging, this.channelHandler, response.intent, response.appMetadata);
1561
1624
  return intentResolution;
1562
1625
  }
1563
1626
  async raiseIntentForContext(context, app) {
@@ -1573,7 +1636,7 @@
1573
1636
  if (response.error) {
1574
1637
  throw new Error(response.error);
1575
1638
  }
1576
- const intentResolution = new ComposeUIIntentResolution(response.messageId, this.jsonMessaging, this.channelFactory, response.intent, response.appMetadata);
1639
+ const intentResolution = new ComposeUIIntentResolution(response.messageId, this.jsonMessaging, this.channelHandler, response.intent, response.appMetadata);
1577
1640
  return intentResolution;
1578
1641
  }
1579
1642
  }
@@ -1802,30 +1865,43 @@
1802
1865
  *
1803
1866
  */
1804
1867
  class ComposeUIDesktopAgent {
1805
- appChannels = [];
1806
- userChannels = [];
1807
- privateChannels = [];
1808
1868
  currentChannel;
1809
1869
  topLevelContextListeners = [];
1810
1870
  intentListeners = [];
1811
- channelFactory;
1871
+ channelHandler;
1812
1872
  intentsClient;
1813
1873
  metadataClient;
1814
1874
  openClient;
1815
1875
  openedAppContext;
1816
1876
  openedAppContextHandled = false;
1817
1877
  //TODO: we should enable passing multiple channelId to the ctor.
1818
- constructor(messaging, channelFactory, intentsClient, metadataClient, openClient) {
1878
+ constructor(messaging, channelHandler, intentsClient, metadataClient, openClient) {
1819
1879
  if (!window.composeui.fdc3.config || !window.composeui.fdc3.config.instanceId) {
1820
1880
  throw new Error(ComposeUIErrors.InstanceIdNotFound);
1821
1881
  }
1822
1882
  const jsonMessaging = new JsonMessaging(messaging);
1823
1883
  // TODO: inject this directly instead of the messageRouter
1824
- this.channelFactory = channelFactory ?? new MessagingChannelFactory(jsonMessaging, window.composeui.fdc3.config.instanceId);
1825
- this.intentsClient = intentsClient ?? new MessagingIntentsClient(jsonMessaging, this.channelFactory);
1884
+ this.channelHandler = channelHandler ?? new MessagingChannelHandler(jsonMessaging, window.composeui.fdc3.config.instanceId);
1885
+ this.intentsClient = intentsClient ?? new MessagingIntentsClient(jsonMessaging, this.channelHandler);
1826
1886
  this.metadataClient = metadataClient ?? new MessagingMetadataClient(jsonMessaging, window.composeui.fdc3.config);
1827
1887
  this.openClient = openClient ?? new MessagingOpenClient(window.composeui.fdc3.config.instanceId, jsonMessaging, window.composeui.fdc3.openAppIdentifier);
1828
1888
  }
1889
+ async [Symbol.asyncDispose]() {
1890
+ console.debug("Disposing ComposeUIDesktopAgent");
1891
+ await this.channelHandler[Symbol.asyncDispose]();
1892
+ for (const listener of this.intentListeners) {
1893
+ await listener.unsubscribe();
1894
+ }
1895
+ this.intentListeners = [];
1896
+ for (const listener of this.topLevelContextListeners) {
1897
+ await listener.unsubscribe();
1898
+ }
1899
+ this.topLevelContextListeners = [];
1900
+ }
1901
+ // This regiters an endpoint to listen when an action was initiated from the UI to select a user channel to join to.
1902
+ async init() {
1903
+ await this.channelHandler.configureChannelSelectorFromUI();
1904
+ }
1829
1905
  async open(app, context) {
1830
1906
  return await this.openClient.open(app, context);
1831
1907
  }
@@ -1851,7 +1927,7 @@
1851
1927
  return await this.intentsClient.raiseIntentForContext(context, app);
1852
1928
  }
1853
1929
  async addIntentListener(intent, handler) {
1854
- var listener = await this.channelFactory.getIntentListener(intent, handler);
1930
+ var listener = await this.channelHandler.getIntentListener(intent, handler);
1855
1931
  this.intentListeners.push(listener);
1856
1932
  return listener;
1857
1933
  }
@@ -1872,7 +1948,7 @@
1872
1948
  //There is no context to handle -aka app was not opened via the fdc3.open
1873
1949
  this.openedAppContextHandled = true;
1874
1950
  }
1875
- const listener = await this.channelFactory.getContextListener(this.openedAppContextHandled, this.currentChannel, handler, contextType);
1951
+ const listener = await this.channelHandler.getContextListener(this.openedAppContextHandled, this.currentChannel, handler, contextType);
1876
1952
  this.topLevelContextListeners.push(listener);
1877
1953
  if (!this.currentChannel) {
1878
1954
  return listener;
@@ -1884,20 +1960,18 @@
1884
1960
  });
1885
1961
  }
1886
1962
  async getUserChannels() {
1887
- return await this.channelFactory.getUserChannels();
1963
+ return await this.channelHandler.getUserChannels();
1888
1964
  }
1889
1965
  async joinUserChannel(channelId) {
1890
1966
  if (this.currentChannel) {
1891
1967
  //DesktopAgnet clients can listen on only one channel
1968
+ console.debug("Leaving current channel: ", this.currentChannel.id);
1892
1969
  await this.leaveCurrentChannel();
1893
1970
  }
1894
- let channel = this.userChannels.find(innerChannel => innerChannel.id == channelId);
1971
+ let channel = await this.channelHandler.joinUserChannel(channelId);
1972
+ console.debug("Joined to user channel: ", channelId);
1895
1973
  if (!channel) {
1896
- channel = await this.channelFactory.joinUserChannel(channelId);
1897
- if (!channel) {
1898
- throw new Error(ChannelError.NoChannelFound);
1899
- }
1900
- this.addChannel(channel);
1974
+ throw new Error(ChannelError.NoChannelFound);
1901
1975
  }
1902
1976
  this.currentChannel = channel;
1903
1977
  for (const listener of this.topLevelContextListeners) {
@@ -1908,26 +1982,25 @@
1908
1982
  }
1909
1983
  }
1910
1984
  async getOrCreateChannel(channelId) {
1911
- let appChannel = this.appChannels.find(channel => channel.id == channelId);
1912
- if (appChannel) {
1913
- return appChannel;
1914
- }
1915
- appChannel = await this.channelFactory.createAppChannel(channelId);
1916
- this.addChannel(appChannel);
1985
+ let appChannel = await this.channelHandler.createAppChannel(channelId);
1917
1986
  return appChannel;
1918
1987
  }
1919
1988
  async createPrivateChannel() {
1920
- return await this.channelFactory.createPrivateChannel();
1989
+ return await this.channelHandler.createPrivateChannel();
1921
1990
  }
1922
1991
  async getCurrentChannel() {
1923
1992
  return this.currentChannel ?? null;
1924
1993
  }
1925
1994
  async leaveCurrentChannel() {
1926
1995
  //The context listeners, that have been added through the `fdc3.addContextListener()` should unsubscribe
1996
+ console.debug("Unsubscribing top level context listeners: ", this.topLevelContextListeners);
1927
1997
  for (const listener of this.topLevelContextListeners) {
1928
1998
  await listener.unsubscribe();
1929
1999
  }
1930
- this.currentChannel = undefined;
2000
+ if (this.currentChannel) {
2001
+ await this.channelHandler.leaveCurrentChannel();
2002
+ this.currentChannel = undefined;
2003
+ }
1931
2004
  }
1932
2005
  async getInfo() {
1933
2006
  return await this.metadataClient.getInfo();
@@ -1953,21 +2026,6 @@
1953
2026
  console.error("The opened app via fdc3.open() could not retrieve the context: ", err);
1954
2027
  }
1955
2028
  }
1956
- addChannel(channel) {
1957
- if (channel == null)
1958
- return;
1959
- switch (channel.type) {
1960
- case "app":
1961
- this.appChannels.push(channel);
1962
- break;
1963
- case "user":
1964
- this.userChannels.push(channel);
1965
- break;
1966
- case "private":
1967
- this.privateChannels.push(channel);
1968
- break;
1969
- }
1970
- }
1971
2029
  async callHandlerOnChannelsCurrentContext(listener) {
1972
2030
  const lastContext = await this.currentChannel.getCurrentContext(listener.contextType);
1973
2031
  if (lastContext) {
@@ -1994,6 +2052,30 @@
1994
2052
  const openAppIdentifier = window.composeui.fdc3.openAppIdentifier;
1995
2053
  const messaging = window.composeui.messaging.communicator;
1996
2054
  const fdc3 = new ComposeUIDesktopAgent(messaging);
2055
+ await fdc3.init();
2056
+ let _disposed = false;
2057
+ const disposeAgent = () => {
2058
+ if (_disposed) {
2059
+ return;
2060
+ }
2061
+ _disposed = true;
2062
+ try {
2063
+ const agent = window.fdc3 || fdc3;
2064
+ if (agent) {
2065
+ agent[Symbol.asyncDispose]();
2066
+ }
2067
+ }
2068
+ catch (err) {
2069
+ console.warn("Error disposing FDC3 agent", err);
2070
+ }
2071
+ finally {
2072
+ // remove handlers after first run
2073
+ window.removeEventListener("beforeunload", disposeAgent);
2074
+ window.removeEventListener("unload", disposeAgent);
2075
+ }
2076
+ };
2077
+ window.addEventListener("beforeunload", disposeAgent);
2078
+ window.addEventListener("unload", disposeAgent);
1997
2079
  if (channelId) {
1998
2080
  await fdc3.joinUserChannel(channelId)
1999
2081
  .then(async () => {
@@ -2001,11 +2083,13 @@
2001
2083
  await fdc3.getOpenedAppContext()
2002
2084
  .then(() => {
2003
2085
  window.fdc3 = fdc3;
2086
+ console.log("FDC3 initialized, handled initial context which initiates that the app was opened via `fdc3.open` and joined to channel: ", channelId, window.fdc3);
2004
2087
  window.dispatchEvent(new Event("fdc3Ready"));
2005
2088
  });
2006
2089
  }
2007
2090
  else {
2008
2091
  window.fdc3 = fdc3;
2092
+ console.log("FDC3 initialized and joined to channel: ", channelId, window.fdc3);
2009
2093
  window.dispatchEvent(new Event("fdc3Ready"));
2010
2094
  }
2011
2095
  });
@@ -2014,11 +2098,13 @@
2014
2098
  if (openAppIdentifier) {
2015
2099
  await fdc3.getOpenedAppContext().then(() => {
2016
2100
  window.fdc3 = fdc3;
2101
+ console.log("FDC3 initialized, handled initial context which initiates that the app was opened via `fdc3.open`: ", window.fdc3);
2017
2102
  window.dispatchEvent(new Event("fdc3Ready"));
2018
2103
  });
2019
2104
  }
2020
2105
  else {
2021
2106
  window.fdc3 = fdc3;
2107
+ console.log("FDC3 initialized: ", window.fdc3);
2022
2108
  window.dispatchEvent(new Event("fdc3Ready"));
2023
2109
  }
2024
2110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morgan-stanley/composeui-fdc3",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.12",
4
4
  "private": false,
5
5
  "description": "FDC3 DesktopAgent implementation for Compose UI",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "@finos/fdc3": "~2.0.3",
21
21
  "@morgan-stanley/composeui-messaging-abstractions": "*",
22
- "rxjs": "^7.8.1"
22
+ "rxjs": "7.8.2"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
@@ -30,16 +30,16 @@
30
30
  "provenance": true
31
31
  },
32
32
  "devDependencies": {
33
- "@rollup/plugin-commonjs": "28.0.6",
34
- "@rollup/plugin-node-resolve": "16.0.1",
33
+ "@rollup/plugin-commonjs": "29.0.0",
34
+ "@rollup/plugin-node-resolve": "16.0.3",
35
35
  "@types/node": "^24.5.2",
36
- "jsdom": "^26.0.0",
37
- "rimraf": "6.0.1",
38
- "rollup": "^4.12.1",
36
+ "jsdom": "^28.1.0",
37
+ "rimraf": "6.1.3",
38
+ "rollup": "4.59.0",
39
39
  "ts-node": "10.9.2",
40
- "tslib": "^2.4.0",
41
- "typescript": "^5.3.3",
42
- "vitest": "^2.0.0"
40
+ "tslib": "2.8.1",
41
+ "typescript": "5.9.3",
42
+ "vitest": "4.0.18"
43
43
  },
44
- "gitHead": "85e43dfe80fbc27986266263d1959fcae91110ac"
44
+ "gitHead": "78d8bf3c57db6188e507391de8dfe1720985c315"
45
45
  }
@@ -11,6 +11,7 @@
11
11
  *
12
12
  */
13
13
 
14
+ import { ContextHandler } from '@finos/fdc3';
14
15
  import { ComposeUIContextListener } from './infrastructure/ComposeUIContextListener';
15
16
  import { Fdc3AddContextListenerResponse } from './infrastructure/messages/Fdc3AddContextListenerResponse';
16
17
  import { IMessaging, JsonMessaging } from '@morgan-stanley/composeui-messaging-abstractions';
@@ -34,14 +35,20 @@ const wrongContext = {
34
35
  type: 'dummy'
35
36
  }
36
37
 
37
- const contextMessageHandlerMock = {
38
- contextHandler: (_: unknown) => {}
39
- };
38
+ export interface ContextHandlerMock {
39
+ contextHandler: ContextHandler;
40
+ }
41
+
42
+ let contextMessageHandlerMock : ContextHandlerMock;
40
43
 
41
44
  describe('Tests for ComposeUIContextListener implementation API', () => {
42
45
 
43
46
  beforeEach(async () => {
44
47
 
48
+ contextMessageHandlerMock = {
49
+ contextHandler: (_: unknown) => {}
50
+ };
51
+
45
52
  // @ts-ignore
46
53
  window.composeui = {
47
54
  fdc3: {
@@ -18,7 +18,7 @@ import { ComposeUIDesktopAgent } from './ComposeUIDesktopAgent';
18
18
  import { ComposeUITopic } from './infrastructure/ComposeUITopic';
19
19
  import { Channel, ChannelError, ContextHandler } from '@finos/fdc3';
20
20
  import { ComposeUIErrors } from './infrastructure/ComposeUIErrors';
21
- import { ChannelFactory } from './infrastructure/ChannelFactory';
21
+ import { ChannelHandler } from './infrastructure/ChannelHandler';
22
22
  import { ComposeUIPrivateChannel } from './infrastructure/ComposeUIPrivateChannel';
23
23
  import { ChannelType } from './infrastructure/ChannelType';
24
24
  import { Fdc3GetOpenedAppContextResponse } from './infrastructure/messages/Fdc3GetOpenedAppContextResponse';
@@ -36,7 +36,7 @@ const testInstrument = {
36
36
 
37
37
  const contextMessageHandlerMock = vi.fn((_ctx) => 'dummy');
38
38
 
39
- const buildChannelFactory = (jm: JsonMessaging): ChannelFactory => ({
39
+ const buildChannelFactory = (jm: JsonMessaging): ChannelHandler => ({
40
40
  createPrivateChannel: vi.fn(() =>
41
41
  Promise.resolve(new ComposeUIPrivateChannel('privateId', 'localInstance', jm, true))
42
42
  ),
@@ -51,7 +51,10 @@ const buildChannelFactory = (jm: JsonMessaging): ChannelFactory => ({
51
51
  getContextListener: vi.fn(
52
52
  (_openHandled: boolean, _channel: Channel, handler: ContextHandler, contextType?: string) =>
53
53
  Promise.resolve(new ComposeUIContextListener(true, jm, handler, contextType))
54
- )
54
+ ),
55
+ leaveCurrentChannel: vi.fn(() => Promise.resolve()),
56
+ configureChannelSelectorFromUI: vi.fn(() => Promise.resolve()),
57
+ [Symbol.asyncDispose]: vi.fn(() => Promise.resolve())
55
58
  });
56
59
 
57
60
  describe('Tests for ComposeUIDesktopAgent implementation API', () => {
@@ -29,8 +29,8 @@ import {
29
29
  import { IMessaging, JsonMessaging } from "@morgan-stanley/composeui-messaging-abstractions";
30
30
  import { ComposeUIContextListener } from './infrastructure/ComposeUIContextListener';
31
31
  import { ComposeUIErrors } from './infrastructure/ComposeUIErrors';
32
- import { ChannelFactory } from './infrastructure/ChannelFactory';
33
- import { MessagingChannelFactory } from './infrastructure/MessagingChannelFactory';
32
+ import { ChannelHandler } from './infrastructure/ChannelHandler';
33
+ import { MessagingChannelHandler } from './infrastructure/MessagingChannelHandler';
34
34
  import { MessagingIntentsClient } from './infrastructure/MessagingIntentsClient';
35
35
  import { IntentsClient } from './infrastructure/IntentsClient';
36
36
  import { MetadataClient } from './infrastructure/MetadataClient';
@@ -38,14 +38,11 @@ import { MessagingMetadataClient } from './infrastructure/MessagingMetadataClien
38
38
  import { OpenClient } from "./infrastructure/OpenClient";
39
39
  import { MessagingOpenClient } from "./infrastructure/MessagingOpenClient";
40
40
 
41
- export class ComposeUIDesktopAgent implements DesktopAgent {
42
- private appChannels: Channel[] = [];
43
- private userChannels: Channel[] = [];
44
- private privateChannels: Channel[] = [];
41
+ export class ComposeUIDesktopAgent implements DesktopAgent, AsyncDisposable {
45
42
  private currentChannel?: Channel;
46
43
  private topLevelContextListeners: ComposeUIContextListener[] = [];
47
44
  private intentListeners: Listener[] = [];
48
- private channelFactory: ChannelFactory;
45
+ private channelHandler: ChannelHandler;
49
46
  private intentsClient: IntentsClient;
50
47
  private metadataClient: MetadataClient;
51
48
  private openClient: OpenClient;
@@ -55,7 +52,7 @@ export class ComposeUIDesktopAgent implements DesktopAgent {
55
52
  //TODO: we should enable passing multiple channelId to the ctor.
56
53
  constructor(
57
54
  messaging: IMessaging,
58
- channelFactory?: ChannelFactory,
55
+ channelHandler?: ChannelHandler,
59
56
  intentsClient?: IntentsClient,
60
57
  metadataClient?: MetadataClient,
61
58
  openClient?: OpenClient) {
@@ -67,12 +64,35 @@ export class ComposeUIDesktopAgent implements DesktopAgent {
67
64
  const jsonMessaging: JsonMessaging = new JsonMessaging(messaging);
68
65
 
69
66
  // TODO: inject this directly instead of the messageRouter
70
- this.channelFactory = channelFactory ?? new MessagingChannelFactory(jsonMessaging, window.composeui.fdc3.config.instanceId);
71
- this.intentsClient = intentsClient ?? new MessagingIntentsClient(jsonMessaging, this.channelFactory);
67
+ this.channelHandler = channelHandler ?? new MessagingChannelHandler(jsonMessaging, window.composeui.fdc3.config.instanceId);
68
+ this.intentsClient = intentsClient ?? new MessagingIntentsClient(jsonMessaging, this.channelHandler);
72
69
  this.metadataClient = metadataClient ?? new MessagingMetadataClient(jsonMessaging, window.composeui.fdc3.config);
73
70
  this.openClient = openClient ?? new MessagingOpenClient(window.composeui.fdc3.config.instanceId!, jsonMessaging, window.composeui.fdc3.openAppIdentifier);
74
71
  }
75
72
 
73
+ public async [Symbol.asyncDispose](): Promise<void> {
74
+ console.debug("Disposing ComposeUIDesktopAgent");
75
+
76
+ await this.channelHandler[Symbol.asyncDispose]();
77
+
78
+ for (const listener of this.intentListeners) {
79
+ await listener.unsubscribe();
80
+ }
81
+
82
+ this.intentListeners = [];
83
+
84
+ for (const listener of this.topLevelContextListeners) {
85
+ await listener.unsubscribe();
86
+ }
87
+
88
+ this.topLevelContextListeners = [];
89
+ }
90
+
91
+ // This regiters an endpoint to listen when an action was initiated from the UI to select a user channel to join to.
92
+ public async init(): Promise<void> {
93
+ await this.channelHandler.configureChannelSelectorFromUI();
94
+ }
95
+
76
96
  public async open(app?: string | AppIdentifier, context?: Context): Promise<AppIdentifier> {
77
97
  return await this.openClient.open(app, context);
78
98
  }
@@ -106,7 +126,7 @@ export class ComposeUIDesktopAgent implements DesktopAgent {
106
126
  }
107
127
 
108
128
  public async addIntentListener(intent: string, handler: IntentHandler): Promise<Listener> {
109
- var listener = await this.channelFactory.getIntentListener(intent, handler);
129
+ var listener = await this.channelHandler.getIntentListener(intent, handler);
110
130
  this.intentListeners.push(listener);
111
131
  return listener;
112
132
  }
@@ -132,7 +152,7 @@ export class ComposeUIDesktopAgent implements DesktopAgent {
132
152
  this.openedAppContextHandled = true;
133
153
  }
134
154
 
135
- const listener = <ComposeUIContextListener>await this.channelFactory.getContextListener(this.openedAppContextHandled, this.currentChannel, handler, contextType);
155
+ const listener = <ComposeUIContextListener>await this.channelHandler.getContextListener(this.openedAppContextHandled, this.currentChannel, handler, contextType);
136
156
  this.topLevelContextListeners.push(listener);
137
157
 
138
158
  if (!this.currentChannel) {
@@ -147,24 +167,22 @@ export class ComposeUIDesktopAgent implements DesktopAgent {
147
167
  }
148
168
 
149
169
  public async getUserChannels(): Promise<Array<Channel>> {
150
- return await this.channelFactory.getUserChannels();
170
+ return await this.channelHandler.getUserChannels();
151
171
  }
152
172
 
153
173
  public async joinUserChannel(channelId: string): Promise<void> {
154
174
  if (this.currentChannel) {
155
175
  //DesktopAgnet clients can listen on only one channel
176
+ console.debug("Leaving current channel: ", this.currentChannel.id);
156
177
  await this.leaveCurrentChannel();
157
178
  }
158
179
 
159
- let channel = this.userChannels.find(innerChannel => innerChannel.id == channelId);
180
+ let channel = await this.channelHandler.joinUserChannel(channelId);
181
+
182
+ console.debug("Joined to user channel: ", channelId);
183
+
160
184
  if (!channel) {
161
- channel = await this.channelFactory.joinUserChannel(channelId);
162
-
163
- if (!channel) {
164
- throw new Error(ChannelError.NoChannelFound);
165
- }
166
-
167
- this.addChannel(channel);
185
+ throw new Error(ChannelError.NoChannelFound);
168
186
  }
169
187
 
170
188
  this.currentChannel = channel;
@@ -178,19 +196,12 @@ export class ComposeUIDesktopAgent implements DesktopAgent {
178
196
  }
179
197
 
180
198
  public async getOrCreateChannel(channelId: string): Promise<Channel> {
181
- let appChannel = this.appChannels.find(channel => channel.id == channelId);
182
- if (appChannel) {
183
- return appChannel;
184
- }
185
-
186
- appChannel = await this.channelFactory.createAppChannel(channelId);
187
-
188
- this.addChannel(appChannel!);
199
+ let appChannel = await this.channelHandler.createAppChannel(channelId);
189
200
  return appChannel!;
190
201
  }
191
202
 
192
203
  public async createPrivateChannel(): Promise<PrivateChannel> {
193
- return await this.channelFactory.createPrivateChannel();
204
+ return await this.channelHandler.createPrivateChannel();
194
205
  }
195
206
 
196
207
  public async getCurrentChannel(): Promise<Channel | null> {
@@ -199,11 +210,15 @@ export class ComposeUIDesktopAgent implements DesktopAgent {
199
210
 
200
211
  public async leaveCurrentChannel(): Promise<void> {
201
212
  //The context listeners, that have been added through the `fdc3.addContextListener()` should unsubscribe
213
+ console.debug("Unsubscribing top level context listeners: ", this.topLevelContextListeners);
202
214
  for (const listener of this.topLevelContextListeners) {
203
215
  await listener.unsubscribe();
204
216
  }
205
217
 
206
- this.currentChannel = undefined;
218
+ if (this.currentChannel) {
219
+ await this.channelHandler.leaveCurrentChannel();
220
+ this.currentChannel = undefined;
221
+ }
207
222
  }
208
223
 
209
224
  public async getInfo(): Promise<ImplementationMetadata> {
@@ -234,21 +249,6 @@ export class ComposeUIDesktopAgent implements DesktopAgent {
234
249
  }
235
250
  }
236
251
 
237
- private addChannel(channel: Channel): void {
238
- if (channel == null) return;
239
- switch (channel.type) {
240
- case "app":
241
- this.appChannels.push(channel);
242
- break;
243
- case "user":
244
- this.userChannels.push(channel);
245
- break;
246
- case "private":
247
- this.privateChannels.push(channel);
248
- break;
249
- }
250
- }
251
-
252
252
  private async callHandlerOnChannelsCurrentContext(listener: ComposeUIContextListener) : Promise<void> {
253
253
  const lastContext = await this.currentChannel!.getCurrentContext(listener.contextType);
254
254
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { describe, it, expect, vi } from 'vitest';
14
14
  import { ChannelError } from '@finos/fdc3';
15
- import { MessagingChannelFactory } from './infrastructure/MessagingChannelFactory';
15
+ import { MessagingChannelHandler } from './infrastructure/MessagingChannelHandler';
16
16
  import { Fdc3JoinUserChannelResponse } from './infrastructure/messages/Fdc3JoinUserChannelResponse';
17
17
  import { Fdc3GetUserChannelsResponse } from './infrastructure/messages/Fdc3GetUserChannelsResponse';
18
18
  import { ComposeUIChannel } from './infrastructure/ComposeUIChannel';
@@ -30,11 +30,11 @@ const baseMessagingMock = (): IMessaging => ({
30
30
  invokeService: vi.fn(() => Promise.resolve(null))
31
31
  });
32
32
 
33
- describe('MessagingChannelFactory tests', () => {
33
+ describe('MessagingChannelHandler tests', () => {
34
34
  it('joinUserChannel rejects CreationFailed when no response', async () => {
35
35
  const messagingMock = baseMessagingMock();
36
36
  const jsonMessaging = new JsonMessaging(messagingMock);
37
- const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance');
37
+ const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance');
38
38
  await expect(factory.joinUserChannel('dummyId')).rejects.toThrow(ChannelError.CreationFailed);
39
39
  expect(messagingMock.invokeService).toHaveBeenCalledTimes(1);
40
40
  });
@@ -44,7 +44,7 @@ describe('MessagingChannelFactory tests', () => {
44
44
  const messagingMock = baseMessagingMock();
45
45
  messagingMock.invokeService = vi.fn(() => Promise.resolve(JSON.stringify(response)));
46
46
  const jsonMessaging = new JsonMessaging(messagingMock);
47
- const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance');
47
+ const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance');
48
48
  await expect(factory.joinUserChannel('dummyId')).rejects.toThrow('testError');
49
49
  expect(messagingMock.invokeService).toHaveBeenCalledTimes(1);
50
50
  });
@@ -57,7 +57,7 @@ describe('MessagingChannelFactory tests', () => {
57
57
  const messagingMock = baseMessagingMock();
58
58
  messagingMock.invokeService = vi.fn(() => Promise.resolve(JSON.stringify(response)));
59
59
  const jsonMessaging = new JsonMessaging(messagingMock);
60
- const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance');
60
+ const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance');
61
61
  await expect(factory.joinUserChannel('dummyId')).rejects.toThrow(ChannelError.CreationFailed);
62
62
  expect(messagingMock.invokeService).toHaveBeenCalledTimes(1);
63
63
  });
@@ -70,16 +70,16 @@ describe('MessagingChannelFactory tests', () => {
70
70
  const messagingMock = baseMessagingMock();
71
71
  messagingMock.invokeService = vi.fn(() => Promise.resolve(JSON.stringify(response)));
72
72
  const jsonMessaging = new JsonMessaging(messagingMock);
73
- const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance');
73
+ const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance');
74
74
  const result = await factory.joinUserChannel('dummyId');
75
75
  expect(result).toBeInstanceOf(ComposeUIChannel);
76
- expect(messagingMock.invokeService).toHaveBeenCalledTimes(1);
76
+ expect(messagingMock.invokeService).toHaveBeenCalledTimes(2); //This will trigger the channel selector logic as well.
77
77
  });
78
78
 
79
79
  it('getUserChannels rejects NoChannelFound when no response', async () => {
80
80
  const messagingMock = baseMessagingMock();
81
81
  const jsonMessaging = new JsonMessaging(messagingMock);
82
- const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance');
82
+ const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance');
83
83
  await expect(factory.getUserChannels()).rejects.toThrow(ChannelError.NoChannelFound);
84
84
  expect(messagingMock.invokeService).toHaveBeenCalledTimes(1);
85
85
  });
@@ -89,7 +89,7 @@ describe('MessagingChannelFactory tests', () => {
89
89
  const messagingMock = baseMessagingMock();
90
90
  messagingMock.invokeService = vi.fn(() => Promise.resolve(JSON.stringify(response)));
91
91
  const jsonMessaging = new JsonMessaging(messagingMock);
92
- const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance');
92
+ const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance');
93
93
  await expect(factory.getUserChannels()).rejects.toThrow('testError');
94
94
  expect(messagingMock.invokeService).toHaveBeenCalledTimes(1);
95
95
  });
@@ -101,7 +101,7 @@ describe('MessagingChannelFactory tests', () => {
101
101
  const messagingMock = baseMessagingMock();
102
102
  messagingMock.invokeService = vi.fn(() => Promise.resolve(JSON.stringify(response)));
103
103
  const jsonMessaging = new JsonMessaging(messagingMock);
104
- const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance');
104
+ const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance');
105
105
  const result = await factory.getUserChannels();
106
106
  expect(result).toBeDefined();
107
107
  expect(result.length).toBe(1);
package/src/index.ts CHANGED
@@ -39,6 +39,33 @@ async function initialize(): Promise<void> {
39
39
  const openAppIdentifier: OpenAppIdentifier | undefined = window.composeui.fdc3.openAppIdentifier;
40
40
  const messaging = window.composeui.messaging.communicator as IMessaging;
41
41
  const fdc3 = new ComposeUIDesktopAgent(messaging);
42
+ await fdc3.init();
43
+
44
+ let _disposed = false;
45
+
46
+ const disposeAgent = () => {
47
+ if (_disposed) {
48
+ return;
49
+ }
50
+
51
+ _disposed = true;
52
+
53
+ try {
54
+ const agent: ComposeUIDesktopAgent = (window.fdc3 as ComposeUIDesktopAgent) || fdc3;
55
+ if (agent) {
56
+ agent[Symbol.asyncDispose]()
57
+ }
58
+ } catch (err) {
59
+ console.warn("Error disposing FDC3 agent", err);
60
+ } finally {
61
+ // remove handlers after first run
62
+ window.removeEventListener("beforeunload", disposeAgent);
63
+ window.removeEventListener("unload", disposeAgent);
64
+ }
65
+ };
66
+
67
+ window.addEventListener("beforeunload", disposeAgent);
68
+ window.addEventListener("unload", disposeAgent);
42
69
 
43
70
  if (channelId) {
44
71
  await fdc3.joinUserChannel(channelId)
@@ -47,10 +74,12 @@ async function initialize(): Promise<void> {
47
74
  await fdc3.getOpenedAppContext()
48
75
  .then(() => {
49
76
  window.fdc3 = fdc3;
77
+ console.log("FDC3 initialized, handled initial context which initiates that the app was opened via `fdc3.open` and joined to channel: ", channelId, window.fdc3);
50
78
  window.dispatchEvent(new Event("fdc3Ready"));
51
79
  })
52
80
  } else {
53
81
  window.fdc3 = fdc3;
82
+ console.log("FDC3 initialized and joined to channel: ", channelId, window.fdc3);
54
83
  window.dispatchEvent(new Event("fdc3Ready"));
55
84
  }
56
85
  });
@@ -58,10 +87,12 @@ async function initialize(): Promise<void> {
58
87
  if (openAppIdentifier) {
59
88
  await fdc3.getOpenedAppContext().then(() => {
60
89
  window.fdc3 = fdc3;
90
+ console.log("FDC3 initialized, handled initial context which initiates that the app was opened via `fdc3.open`: ", window.fdc3);
61
91
  window.dispatchEvent(new Event("fdc3Ready"));
62
92
  })
63
93
  } else {
64
94
  window.fdc3 = fdc3;
95
+ console.log("FDC3 initialized: ", window.fdc3);
65
96
  window.dispatchEvent(new Event("fdc3Ready"));
66
97
  }
67
98
  }
@@ -14,12 +14,49 @@
14
14
  import { Channel, ContextHandler, IntentHandler, Listener, PrivateChannel } from "@finos/fdc3";
15
15
  import { ChannelType } from "./ChannelType";
16
16
 
17
- export interface ChannelFactory {
17
+ export interface ChannelHandler extends AsyncDisposable {
18
+ /*
19
+ * Gets a channel by sending a request to the backend using its ID and type
20
+ */
18
21
  getChannel(channelId: string, channelType: ChannelType): Promise<Channel>;
22
+
23
+ /*
24
+ * Creates a private channel by sending a request to the backend
25
+ */
19
26
  createPrivateChannel(): Promise<PrivateChannel>;
27
+
28
+ /*
29
+ * Creates an app channel by sending a request to the backend using its ID
30
+ */
20
31
  createAppChannel(channelId: string): Promise<Channel>;
32
+
33
+ /*
34
+ * Joins a user channel by sending a request to the backend using its ID
35
+ */
21
36
  joinUserChannel(channelId: string): Promise<Channel>;
37
+
38
+ /*
39
+ * Gets all the user channels by sending a request to the backend
40
+ */
22
41
  getUserChannels(): Promise<Channel[]>;
42
+
43
+ /*
44
+ * Gets all the app channels by sending a request to the backend
45
+ */
23
46
  getIntentListener(intent: string, handler: IntentHandler): Promise<Listener>;
47
+
48
+ /*
49
+ * Gets a context listener by sending a request to the backend. This should reflect if the initial context sent by the fdc3.open call was handled or not.
50
+ */
24
51
  getContextListener(openHandled: boolean, channel?: Channel, handler?: ContextHandler, contextType?: string | null): Promise<Listener>;
52
+
53
+ /*
54
+ * Configures the channel selector to allow the user to select a channel from the UI, by registering an endpoint to listen to UI initiated actions.
55
+ */
56
+ configureChannelSelectorFromUI(): Promise<void>;
57
+
58
+ /*
59
+ * Leaves the current channel by sending a request to the backend using its ID
60
+ */
61
+ leaveCurrentChannel(): Promise<void>;
25
62
  }
@@ -13,6 +13,9 @@
13
13
  import { DisplayMetadata } from "@finos/fdc3";
14
14
  import { ChannelType } from "./ChannelType";
15
15
 
16
+ /*
17
+ * Represents a channel item containing its id, type and optional display metadata
18
+ */
16
19
  export interface ChannelItem {
17
20
  id: string;
18
21
  type: ChannelType;
@@ -35,7 +35,7 @@ export class ComposeUIChannel implements Channel {
35
35
  this.displayMetadata = displayMetadata;
36
36
  }
37
37
 
38
- //Broadcasting on the composeui/fdc3/v2.0/broadcast topic
38
+ //Broadcasting on the composeui/fdc3/v2.0/<channel>/broadcast topic
39
39
  public async broadcast(context: Context): Promise<void> {
40
40
  //Setting the last published context message.
41
41
  this.lastContexts.set(context.type, context);
@@ -42,6 +42,9 @@ export class ComposeUIContextListener implements Listener {
42
42
 
43
43
  public async subscribe(channelId: string, channelType: ChannelType): Promise<void> {
44
44
  await this.registerContextListener(channelId, channelType);
45
+
46
+ console.debug("Subscribed context listener with id: ", this.id, ", to channel: ", channelId, ", of type: ", channelType, ", for context type: ", this.contextType);
47
+
45
48
  const subscribeTopic = ComposeUITopic.broadcast(channelId, channelType);
46
49
 
47
50
  this.unsubscribable = await this.jsonMessaging.subscribeJson<Context>(subscribeTopic, async (context: Context) => {
@@ -54,6 +57,7 @@ export class ComposeUIContextListener implements Listener {
54
57
  }
55
58
  });
56
59
 
60
+ console.log("Registered context listener with id: ", this.id, ", to topic: ", subscribeTopic);
57
61
  this.isSubscribed = true;
58
62
  }
59
63
 
@@ -94,16 +98,18 @@ export class ComposeUIContextListener implements Listener {
94
98
 
95
99
  public async unsubscribe(): Promise<void> {
96
100
  if (!this.unsubscribable || !this.isSubscribed) {
101
+ console.debug("The current listener is not subscribed.");
97
102
  return;
98
103
  }
99
104
 
100
105
  try {
101
106
  await this.leaveChannel();
107
+ console.debug("Unsubscribed context listener with id: ", this.id);
102
108
  } catch(err) {
103
109
  console.log(err);
104
110
  }
105
111
 
106
- this.unsubscribable.unsubscribe();
112
+ await this.unsubscribable.unsubscribe();
107
113
  this.isSubscribed = false;
108
114
 
109
115
  if (this.unsubscribeCallback) {
@@ -129,6 +135,7 @@ export class ComposeUIContextListener implements Listener {
129
135
  }
130
136
 
131
137
  private async leaveChannel() : Promise<void> {
138
+ console.debug("Removing context listener with id: ", this.id);
132
139
  const request = new Fdc3RemoveContextListenerRequest(window.composeui.fdc3.config?.instanceId!, this.id!, this.contextType);
133
140
  const response = await this.jsonMessaging.invokeJsonService<Fdc3RemoveContextListenerRequest, Fdc3RemoveContextListenerResponse>(ComposeUITopic.removeContextListener(), request);
134
141
  if (!response) {
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { AppMetadata, IntentResolution, IntentResult } from "@finos/fdc3";
14
14
  import { JsonMessaging } from "@morgan-stanley/composeui-messaging-abstractions";
15
- import { ChannelFactory } from "./ChannelFactory";
15
+ import { ChannelHandler } from "./ChannelHandler";
16
16
  import { ComposeUIErrors } from "./ComposeUIErrors";
17
17
  import { ComposeUITopic } from "./ComposeUITopic";
18
18
  import { Fdc3GetIntentResultRequest } from "./messages/Fdc3GetIntentResultRequest";
@@ -20,18 +20,18 @@ import { Fdc3GetIntentResultResponse } from "./messages/Fdc3GetIntentResultRespo
20
20
 
21
21
  export class ComposeUIIntentResolution implements IntentResolution {
22
22
  private jsonMessaging: JsonMessaging;
23
- private channelFactory: ChannelFactory;
23
+ private channelHandler: ChannelHandler;
24
24
  public source: AppMetadata;
25
25
  public intent: string
26
26
  public messageId: string;
27
27
 
28
28
 
29
- constructor(messageId: string, jsonMessaging: JsonMessaging, channelFactory: ChannelFactory, intent: string, source: AppMetadata) {
29
+ constructor(messageId: string, jsonMessaging: JsonMessaging, channelHandler: ChannelHandler, intent: string, source: AppMetadata) {
30
30
  this.messageId = messageId;
31
31
  this.intent = intent;
32
32
  this.source = source;
33
33
  this.jsonMessaging = jsonMessaging;
34
- this.channelFactory = channelFactory;
34
+ this.channelHandler = channelHandler;
35
35
  }
36
36
 
37
37
  async getResult(): Promise<IntentResult> {
@@ -47,7 +47,7 @@ export class ComposeUIIntentResolution implements IntentResolution {
47
47
  }
48
48
 
49
49
  if (result.channelId && result.channelType) {
50
- const channel = this.channelFactory.getChannel(result.channelId, result.channelType)
50
+ const channel = this.channelHandler.getChannel(result.channelId, result.channelType)
51
51
  return channel;
52
52
  } else if (result.context) {
53
53
  return result.context;
@@ -104,7 +104,7 @@ export class ComposeUIPrivateChannel extends ComposeUIChannel implements Private
104
104
  if (this.disconnected) {
105
105
  throw new Error("Channel disconnected");
106
106
  }
107
-
107
+
108
108
  return super.broadcast(context)
109
109
  }
110
110
 
@@ -107,6 +107,14 @@ export class ComposeUITopic {
107
107
  return `${this.topicRoot}/${this.joinUserChannelSuffix}`;
108
108
  }
109
109
 
110
+ public static channelSelectorFromUI(instanceId: string): string {
111
+ return `${this.topicRoot}/channelSelector/UI/${instanceId}`;
112
+ }
113
+
114
+ public static channelSelectorFromAPI(instanceId: string): string {
115
+ return `${this.topicRoot}/channelSelector/API/${instanceId}`;
116
+ }
117
+
110
118
  public static getInfo(): string {
111
119
  return `${this.topicRoot}/${this.getInfoSuffix}`;
112
120
  }
@@ -14,7 +14,7 @@
14
14
  import { ChannelError, ContextHandler, IntentHandler, Listener, PrivateChannel } from "@finos/fdc3";
15
15
  import { JsonMessaging } from "@morgan-stanley/composeui-messaging-abstractions";
16
16
  import { Channel } from "@finos/fdc3";
17
- import { ChannelFactory } from "./ChannelFactory";
17
+ import { ChannelHandler } from "./ChannelHandler";
18
18
  import { ComposeUIPrivateChannel } from "./ComposeUIPrivateChannel";
19
19
  import { Fdc3CreatePrivateChannelRequest } from "./messages/Fdc3CreatePrivateChannelRequest";
20
20
  import { Fdc3CreatePrivateChannelResponse } from "./messages/Fdc3CreatePrivateChannelResponse";
@@ -38,14 +38,26 @@ import { Fdc3JoinUserChannelResponse } from "./messages/Fdc3JoinUserChannelRespo
38
38
  import { ChannelItem } from "./ChannelItem";
39
39
  import { ComposeUIContextListener } from "./ComposeUIContextListener";
40
40
 
41
- export class MessagingChannelFactory implements ChannelFactory {
41
+ export class MessagingChannelHandler implements ChannelHandler {
42
42
  private jsonMessaging: JsonMessaging;
43
43
  private fdc3instanceId: string;
44
+ private channelSelector: AsyncDisposable | undefined = undefined;
44
45
 
45
46
  constructor(jsonMessaging: JsonMessaging,fdc3instanceId: string) {
46
47
  this.jsonMessaging = jsonMessaging;
47
48
  this.fdc3instanceId = fdc3instanceId;
48
49
  }
50
+
51
+ [Symbol.asyncDispose](): PromiseLike<void> {
52
+ return this.channelSelector
53
+ ? this.channelSelector[Symbol.asyncDispose]()
54
+ : Promise.resolve();
55
+ }
56
+
57
+ public async configureChannelSelectorFromUI(): Promise<void> {
58
+ this.channelSelector = await this.jsonMessaging.registerService(ComposeUITopic.channelSelectorFromUI(this.fdc3instanceId), this.selectUserChannelFromUIHandler);
59
+ console.debug("Configured channel selector for module: ", this.fdc3instanceId);
60
+ }
49
61
 
50
62
  public async getChannel(channelId: string, channelType: ChannelType): Promise<Channel> {
51
63
  const topic = ComposeUITopic.findChannel();
@@ -117,24 +129,43 @@ export class MessagingChannelFactory implements ChannelFactory {
117
129
  }
118
130
 
119
131
  public async joinUserChannel(channelId: string): Promise<Channel> {
120
- const topic: string = ComposeUITopic.joinUserChannel();
121
- const request: Fdc3JoinUserChannelRequest = new Fdc3JoinUserChannelRequest(channelId, this.fdc3instanceId);
122
- const response = await this.jsonMessaging.invokeJsonService<Fdc3JoinUserChannelRequest, Fdc3JoinUserChannelResponse>(topic, request);
132
+ try {
133
+ const topic: string = ComposeUITopic.joinUserChannel();
134
+ const request: Fdc3JoinUserChannelRequest = new Fdc3JoinUserChannelRequest(channelId, this.fdc3instanceId);
135
+ const response = await this.jsonMessaging.invokeJsonService<Fdc3JoinUserChannelRequest, Fdc3JoinUserChannelResponse>(topic, request);
123
136
 
124
- if (!response) {
125
- throw new Error(ChannelError.CreationFailed);
126
- }
137
+ console.debug("Received joinUserChannel response: ", response);
127
138
 
128
- if (response.error) {
129
- throw new Error(response.error);
130
- }
139
+ if (!response) {
140
+ throw new Error(ChannelError.CreationFailed);
141
+ }
131
142
 
132
- if (!response.success) {
133
- throw new Error(ChannelError.CreationFailed);
143
+ if (response.error) {
144
+ throw new Error(response.error);
145
+ }
146
+
147
+ if (!response.success) {
148
+ throw new Error(ChannelError.CreationFailed);
149
+ }
150
+
151
+ var channel = new ComposeUIChannel(channelId, "user", this.jsonMessaging, response.displayMetadata);
152
+
153
+ this.triggerChannelJoinedEvent(channel.id);
154
+
155
+ return channel;
156
+ } catch (error) {
157
+ console.error("Error joining user channel: ", error);
158
+ throw error;
134
159
  }
160
+ }
135
161
 
136
- var channel = new ComposeUIChannel(channelId, "user", this.jsonMessaging, response.displayMetadata);
137
- return channel;
162
+ private async triggerChannelJoinedEvent(id: string | undefined) : Promise<void> {
163
+ try {
164
+ var result = await this.jsonMessaging.invokeService(ComposeUITopic.channelSelectorFromAPI(this.fdc3instanceId), id);
165
+ console.debug("Triggered channel selector of container: ", this.fdc3instanceId, ", with: ", id, ", and got result:", result);
166
+ } catch (error) {
167
+ console.error("Error triggering channel joined event for module: ", this.fdc3instanceId, ", with channel id: ", id, ", error: ", error);
168
+ }
138
169
  }
139
170
 
140
171
  public async getUserChannels(): Promise<Channel[]> {
@@ -193,4 +224,36 @@ export class MessagingChannelFactory implements ChannelFactory {
193
224
  const listener = new ComposeUIContextListener(openHandled, this.jsonMessaging, handler!, contextType ?? undefined);
194
225
  return listener;
195
226
  }
227
+
228
+ public async leaveCurrentChannel(): Promise<void> {
229
+ await this.triggerChannelJoinedEvent(undefined);
230
+ }
231
+
232
+ private async selectUserChannelFromUIHandler(request?: string | null | undefined): Promise<string | null> {
233
+ try {
234
+ if (!request) {
235
+ console.debug("Empty request received when the user channel selection was requested from UI.");
236
+ return null;
237
+ }
238
+
239
+ var objectRequest = JSON.parse(request);
240
+ var joinUserChannelRequest = objectRequest as Fdc3JoinUserChannelRequest;
241
+ console.debug("Parsed the request from the UI when user selected a user channel to join: ", joinUserChannelRequest);
242
+
243
+ if (!joinUserChannelRequest || !joinUserChannelRequest.channelId) {
244
+ console.debug("Invalid request received when user selected a user channel to join to from the UI: ", request);
245
+ return null;
246
+ }
247
+
248
+ //We should join the channel requested by the user from the UI. -> this will trigger the handler of the module/container to show which channel the app is joined to.
249
+ await window.fdc3.joinUserChannel(joinUserChannelRequest.channelId);
250
+
251
+ return joinUserChannelRequest.channelId;
252
+
253
+ } catch (error) {
254
+ console.error("Error processing request when channel selector was invoked: ", request, ", error: ", error);
255
+ return null;
256
+ }
257
+ }
196
258
  }
259
+
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { AppIdentifier, AppIntent, AppMetadata, Context, IntentResolution } from "@finos/fdc3";
15
15
  import { JsonMessaging } from "@morgan-stanley/composeui-messaging-abstractions";
16
- import { ChannelFactory } from "./ChannelFactory";
16
+ import { ChannelHandler } from "./ChannelHandler";
17
17
  import { ComposeUIErrors } from "./ComposeUIErrors";
18
18
  import { ComposeUIIntentResolution } from "./ComposeUIIntentResolution";
19
19
  import { ComposeUITopic } from "./ComposeUITopic";
@@ -27,15 +27,15 @@ import { Fdc3RaiseIntentResponse } from "./messages/Fdc3RaiseIntentResponse";
27
27
  import { Fdc3RaiseIntentForContextRequest } from "./messages/Fdc3RaiseIntentForContextRequest";
28
28
 
29
29
  export class MessagingIntentsClient implements IntentsClient {
30
- private channelFactory: ChannelFactory;
30
+ private channelHandler: ChannelHandler;
31
31
  private jsonMessaging: JsonMessaging;
32
32
 
33
- constructor( jsonMessaging: JsonMessaging, channelFactory: ChannelFactory, ) {
33
+ constructor( jsonMessaging: JsonMessaging, channelFactory: ChannelHandler, ) {
34
34
  if (!window.composeui.fdc3.config || !window.composeui.fdc3.config.instanceId) {
35
35
  throw new Error(ComposeUIErrors.InstanceIdNotFound);
36
36
  }
37
37
 
38
- this.channelFactory = channelFactory;
38
+ this.channelHandler = channelFactory;
39
39
  this.jsonMessaging = jsonMessaging;
40
40
  }
41
41
 
@@ -69,7 +69,7 @@ export class MessagingIntentsClient implements IntentsClient {
69
69
  }
70
70
 
71
71
  public async getIntentResolution(messageId: string, intent: string, source: AppMetadata): Promise<IntentResolution> {
72
- return new ComposeUIIntentResolution(messageId, this.jsonMessaging, this.channelFactory, intent, source);
72
+ return new ComposeUIIntentResolution(messageId, this.jsonMessaging, this.channelHandler, intent, source);
73
73
  }
74
74
 
75
75
  public async raiseIntent(intent: string, context: Context, app?: string | AppIdentifier): Promise<IntentResolution> {
@@ -88,7 +88,7 @@ export class MessagingIntentsClient implements IntentsClient {
88
88
  throw new Error(response.error);
89
89
  }
90
90
 
91
- const intentResolution = new ComposeUIIntentResolution(response.messageId, this.jsonMessaging, this.channelFactory, response.intent!, response.appMetadata!);
91
+ const intentResolution = new ComposeUIIntentResolution(response.messageId, this.jsonMessaging, this.channelHandler, response.intent!, response.appMetadata!);
92
92
  return intentResolution;
93
93
  }
94
94
 
@@ -113,7 +113,7 @@ export class MessagingIntentsClient implements IntentsClient {
113
113
  throw new Error(response.error);
114
114
  }
115
115
 
116
- const intentResolution = new ComposeUIIntentResolution(response.messageId!, this.jsonMessaging, this.channelFactory, response.intent!, response.appMetadata!);
116
+ const intentResolution = new ComposeUIIntentResolution(response.messageId!, this.jsonMessaging, this.channelHandler, response.intent!, response.appMetadata!);
117
117
  return intentResolution;
118
118
  }
119
119
  }