Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

App instance proposal + connect to app instance for intent results #287

Closed
wants to merge 4 commits into from

Conversation

nkolba
Copy link
Contributor

@nkolba nkolba commented Nov 22, 2020

This is an initial PR to introduce the concept of AppInstances to address data returning intents - as outlined in issue #201 as well as questions around app instances (e.g. issue #231).

Key points of this PR are:

  • introduction of an AppInstance class that provides a reference to a specific instance of an app and allows direct broadcasting / listening of context with that instance
  • IntentResolution source properties can be used to obtain an AppInstance
  • ContextHandler functions now get an additional source arg that provides a reference to the instance sending the context or intent
  • Instances provide lifecycle information

Copy link
Contributor

@mattjamieson mattjamieson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to start to define AppInstance as a concept, definitely seems like something we've been touching on more and more in conversations lately.

Added some comments inline.

@@ -10,6 +10,7 @@ hide_title: true
interface DesktopAgent {
// apps
open(name: string, context?: Context): Promise<void>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could open also return Promise<AppInstance>? Or at least an equivalent to the source property of an IntentResolution?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that would make sense. I think returning the instance itself would be appropriate for open and consistent with window.open in the DOM.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattjamieson updated PR

* ```
*
*/
getAppInstance(instanceId : string ) : Promise<AppInstance>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For getCurrentChannel we specify returning null if the channel doesn't exist (although the return type doesn't allow it, and I think there is a separate PR to fix that). Should we do that here too?:

getAppInstance(instanceId: string) : Promise<AppInstance | null>;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattjamieson after some more thought, I think we should leave as is. If the instance doesn't exist, you can still get an instance reference and get lifecycle events if it loads, set handlers on it, etc.


Returns the instance of an app for a given identifier.

An instanceId can be obtained from
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #279 we're adding:

type Target = string | AppMetadata;

If/when we merge that we should clarify the language here as I think it only makes sense to getAppInstance with a string appId and not an AppMetadata object?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the instanceId here is different from AppMetadata being provided for the Target.

In the case of Target, we are addressing the app - regardless of instance - and matching on either name/appId or metadata and can return any instance (or even spawn a new one).

Copy link
Contributor

@mattjamieson mattjamieson Dec 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree - it's just that #279 changed IntentResolution.source to be of type Target so we probably need to be clear that this usage is only for the string variant.

Or:

type TargetSource = string;
type Target = TargetSource | AppMetadata;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

k. maybe it would make more sense if there was a type of InstanceId - instead of "TargetSource". e.g

type InstanceId = string;
type Target = InstanceId | AppMetadata;

This would make the type names more aligned and accurate. (what you are targeting above would either be an exact InstanceId or a match on AppMetadata)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Target is actually set to change to TargetApp for clarity.

Might not be altogether cleaner to the instanceId to the AppMetadata object and retire the string? Or have an InstanceMetadata object that includes both the instanceId and AppMetadata.

I'm also wary of returning only the instanceId with no associated metadata identifying the app as no method is provided to retrieve said metadata for an AppInstance. Either the AppInstance itself should contain that or we should be returning it with the InstanceId as a metadata object.


//methods
addContextListener(handler: ContextHandler): Listener;
addContextListener(contextType: string, handler: ContextHandler): Listener;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we should fix the arg order for all the addContextListener methods 😁

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2.0?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They actually cause a lot of problems, being out of order. The code you have to write to deal with it is really ugly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rikoe @mattjamieson - if we made this change in 1.2, what would be the impact to existing apps? Can some of the platform owners weigh in? @kriswest @lspiro-Tick42 @tim-dinsdale

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Speaking of jumping through hoops to make function signatures work, the API would be easier to implement and more consistent if addContextListener() were to return a Promise<Listener> rather than a Listener.

interface AppInstance {
//properties
readonly instanceId : string;
readonly status : 'ready' | 'loading' | 'unregistered';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we type the status?

type AppInstanceStatus = 'ready' | 'loading' | 'unregistered';

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I was being lazy...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattjamieson updated PR

addContextListener(handler: ContextHandler): Listener;
addContextListener(contextType: string, handler: ContextHandler): Listener;
broadcast(context: Context): void;
onStatusChanged(handler : (newVal : string, oldVal :string) => {}) : void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above:

onStatusChanged(handler : (newVal : AppInstanceStatus, oldVal :AppInstanceStatus) => void) : void;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattjamieson updated PR

Copy link
Contributor

@rikoe rikoe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me @nkolba - should we remove the data property from IntentResolution as part of this?

Using FDC3 APIs, an app can get a reference to an instance of another app that it has either raised an intent to or recieved an intent or context from. The `AppInstance` API allows apps to listen for and broadcast context directly.

### Broadcasting and Listening for Context
An AppInstance MUST support both *broadcast* and *addContextListener* methods. Calling *broadcast* on an AppInstance MUST send the context only *to* that instance. The *addContextListener* method MUST listen for context events only *from* that instance.
Copy link

@pgn-vole pgn-vole Dec 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addContextListener method MUST listen for context events only from that instance.

How can the application instance, when using fdc3.broadcast, differenciate between broadcasting to a channel, that it has joined previously, and broadcasting to an actual "appInstance listener"?
I understand that the application instance can do that differenciation with the fdc3.addContextListener API because the handler does provide a source property, which hints on wether the new context is or is not coming from a channel broadcasting. But it is not clear on how this distinction can be done with fdc3.broadcast.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pgn-vole instance messages can only be handled/passed via addContextListener and broadcast on the instance. So, fdc3.broadcast would never get routed to an instance.addContextListener handler and vice-versa. This is consistent with broadcast/handler behavior on channels. The only exception being when an app has actually joined a channel - in which case - the channel context is the same as the global broadcast. I will look amending the spec to make that more clear.

BTWY - The source property won't provide this differentiation - you will always have a single instance source - whether this is coming from a channel, a direct message from an instance, or a global broadcast. The distinction is only in where the app sets the listener.

Copy link

@pgn-vole pgn-vole Jan 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation. So for an application instance to broadcast a message to any of its listeners it must beforehand retrieve its own instance and then broadcast via it like the below?

Listener:

const chart = await fdc3.raiseIntent('ViewChart');
const instance = await fdc3.getAppInstance(chart.source);

instance.addContextListener("someContext", handler);

"ViewChart" Application:

const instance = await fdc3.getAppInstance(myInstanceId); // myInstanceId is exposed in some way by the desktop agent

instance.broadcast({type: "someContext"}) //broadcast its app instance listeners
fdc3.broadcast({type: "someContext"})// broadcast to currently joined channel

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exactly. An app shouldn't broadcast on its own instance - but instead, broadcast on the instance of a subscribing app (retrieved from the intentListener or contextListener). So, in the "ViewChart" example above, you would do this instead:

fdc3.addIntentListener("ViewChart",async (context, source) => {
   const sourceInstance = await fdc3.getAppInstance(source);
   sourceInstance.broadcast({type: "someContext"})
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks that makes more sense. I got confused by the section "Context last value cache" which made me assume that there would be a single context per application instance, however it seems that instead there will be a context held for each relation provider/subscriber.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had similar issues with initial comprehension here, due to the slightly unconventional (but perhaps necessary) approach.

@nkolba When you say that (the app should) "broadcast on the instance of a subscribing app (retrieved from the intentListener or contextListener)" how does it do that retrieval? It would have needed to register a listener of some sort for the addition of those listeners in order to retrieve their IDs. That seems too complex to be practical.

Perhaps what @pgn-vole suggests is simpler (retrieving its own AppInstance and broadcasting on that) OR perhaps some object is returned by addIntentListener for that purpose (this does not solve the open case however).

@rikoe rikoe mentioned this pull request Jan 7, 2021
19 tasks

AppInstances can be used to directly listen for and broadcast context between two apps connected to a desktop agent.

An AppInstance is obtained by calling the `getAppInstence` method on the `DesktopAgent` using a source token provided by either an IntentResolution or as an argument to a ContextHandler.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a small typo: getAppInstence should be getAppInstance

@rikoe rikoe added this to the 2.0 milestone Jan 26, 2021
@rikoe rikoe mentioned this pull request Jan 27, 2021
6 tasks
@rikoe rikoe added the api FDC3 API Working Group label Jan 27, 2021
Copy link
Contributor

@kriswest kriswest left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great start @nkolba.

The biggest outstanding issues I/we see are:

  • the approach is unconventional (though necessary), leading to some problems with comprehension.
    • This might be resolved through documentation and naming conventions (e.g. replacing source in IntentResolution with appInstanceId.
  • The return types of open and raiseIntent are inconsistent. open returns a Promise<AppInstance | null> while raiseIntent returns an IntentResolution containing a source string (presumed to have changed from the apps name to appInstanceId). These should be harmonized.
  • There is no way to retrieve the AppMetadata associated with an InstanceId - this could be solved by:
    • including the AppMetadata in the AppInstance objects returned
    • OR by adding the InstanceId to the AppMetadata object and returning that
    • OR by creating an InstanceMetadata object that wraps the InstanceId and an AppMetadata object
  • There is some inconsistency arising from the fact that raiseIntent accepts a Target (see Adds a new Target type, updates open() and raiseIntent() #279) to be renamed TargetApp (feat: rename Target to TargetApp and update docs to match API #315) but broadcast and addContextListener are called on the AppInstance.
    • The proposal should incorporate the merged changes in Adds a new Target type, updates open() and raiseIntent() #279 and proposed improvement in feat: rename Target to TargetApp and update docs to match API #315
    • we should consider whether it would be more consistent to:
      • further refine the TargetApp to allow use of InstanceMetadata or an AppMetadata with instance id
      • OR allow raiseIntent on the AppInstance object (as the proposal does with broadcast and addContextListener)
      • OR add a target option to the desktop agent's broadcast and addContextListener functions instead of creating an AppInstance object at all (i.e. redefine this proposal using instance metadata only and not an object.
  • The proposed method of responding to an app that has called addContextListener on an AppInstance (retrieving that app's own AppInstance and broadcasting to it) has issues.
    • The responding app currently has no way to get the InstanceId of the subscribing app in order to retrieve it.
      • In the case of fdc3.open, to resolve the open issues, an app would have to register a listener for subscriptions and then retain Ids or AppInstance objects to use for this purpose.
      • In the case of fdc3.raiseIntent the ContextHandler argument on addIntentListener would need to have an additional argument added to it to receive the source instanceId.
      • This feels far too complex to be practical.
    • As this is the primary usecase being addressed this is perhaps the most important issue to address in the proposal.


Returns the instance of an app for a given identifier.

An instanceId can be obtained from
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Target is actually set to change to TargetApp for clarity.

Might not be altogether cleaner to the instanceId to the AppMetadata object and retire the string? Or have an InstanceMetadata object that includes both the instanceId and AppMetadata.

I'm also wary of returning only the instanceId with no associated metadata identifying the app as no method is provided to retrieve said metadata for an AppInstance. Either the AppInstance itself should contain that or we should be returning it with the InstanceId as a metadata object.

* An interface that relates an instance of an app to other apps
*/
export interface AppInstance {
readonly instanceId: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider augmenting this interface with AppMetadata to identify the app that this is an instance of.

The AppMetadata could wrap the instanceId or be a sibling of it

export const open: (
name: string,
context?: Context
) => Promise<AppInstance | null> = (name, context) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is inconsistent with IntentResolution which only returns a source (whose value changes, I assume from the app name (which was not sufficient to uniquely identify an app in either a single or multiple appDs anyway) to an instanceId and requires you to getAppInstance. Either both should return the InstanceId or both should return a Promise<AppInstance | null>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intent resolution was handled in this way to preserve backwards compatibility. Given that this change is being pushed to a 2.x, then breaking changes to IntentResolution could be on the table. Have the intent resolve to an AppInstance (consistent with open) seems to make sense.

// do some vendor-specific stuff

//get an AppInstance object back from the DesktopAgent
const instance = await fdc3.getAppInstance(chart.source);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redefines the meaning of IntentResoluton.source to an InstanceId (currently an app 'name'). Consider adding a different field and/or metadata object for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment above. Also, note that 'source' was never specifically defined as app 'name' - left up to implementation by design.

Using FDC3 APIs, an app can get a reference to an instance of another app that it has either raised an intent to or recieved an intent or context from. The `AppInstance` API allows apps to listen for and broadcast context directly.

### Broadcasting and Listening for Context
An AppInstance MUST support both *broadcast* and *addContextListener* methods. Calling *broadcast* on an AppInstance MUST send the context only *to* that instance. The *addContextListener* method MUST listen for context events only *from* that instance.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had similar issues with initial comprehension here, due to the slightly unconventional (but perhaps necessary) approach.

@nkolba When you say that (the app should) "broadcast on the instance of a subscribing app (retrieved from the intentListener or contextListener)" how does it do that retrieval? It would have needed to register a listener of some sort for the addition of those listeners in order to retrieve their IDs. That seems too complex to be practical.

Perhaps what @pgn-vole suggests is simpler (retrieving its own AppInstance and broadcasting on that) OR perhaps some object is returned by addIntentListener for that purpose (this does not solve the open case however).

@kriswest
Copy link
Contributor

On further review, we also note that there is a danger of an app losing the association of a raised intent with a response with this approach, which implements communications between two instances after an intent is raised, but doesn't necessarily relate those comms to a specific intent. What happens if an app raises more than one intent and targets the same instance? The same conduit would have to be used to reply to both intents, with messages potentially being interleaved and needing additional context metadata to relate them to the specific intents.

However, there already exists the concepts of app channels that could be used to implement the comms between instances. As an application can create and join more than one channel it can uniquely tie a stream of responses to a single event. Alternatively, it can also reuse a channel (e.g. if more than one app requests the same pricing stream).

Using channels would also eliminate the duplication of functionality that the proposed AppInstances create (you can already broadcast and addContextListener on the desktop agent and channels, this proposal would create a third set of these functions). Working with a channel returned by raiseIntent or open would also be easier to understand conceptually.

@rikoe rikoe changed the title final draft additions for AppInstance spec App instance proposal with ability to connect to app instances for listening to intent results Apr 8, 2021
@rikoe rikoe changed the title App instance proposal with ability to connect to app instances for listening to intent results App instance proposal + connect to app instance for intent results Apr 8, 2021
@rikoe
Copy link
Contributor

rikoe commented Oct 11, 2021

@nkolba we propose closing this PR as it has been inactive for a while, and due to the comments above, and potential issues that was raised - which do not have a good solution at the moment - we were not able to reach consensus at the Standard Working Group on accepting this PR.

We will continue to explore ways to achieve the proposals raised in this PR in the bi-weekly Channels & Feeds Discussion Group, please feel free to participate in those discussions.

@kriswest kriswest closed this Nov 12, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api FDC3 API Working Group cla-present
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants