-
Notifications
You must be signed in to change notification settings - Fork 132
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
Conversation
There was a problem hiding this 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.
docs/api/ref/DesktopAgent.md
Outdated
@@ -10,6 +10,7 @@ hide_title: true | |||
interface DesktopAgent { | |||
// apps | |||
open(name: string, context?: Context): Promise<void>; |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mattjamieson updated PR
src/api/DesktopAgent.ts
Outdated
* ``` | ||
* | ||
*/ | ||
getAppInstance(instanceId : string ) : Promise<AppInstance>; |
There was a problem hiding this comment.
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>;
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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;
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2.0?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
.
docs/api/ref/AppInstance.md
Outdated
interface AppInstance { | ||
//properties | ||
readonly instanceId : string; | ||
readonly status : 'ready' | 'loading' | 'unregistered'; |
There was a problem hiding this comment.
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';
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mattjamieson updated PR
There was a problem hiding this 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. |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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"})
});
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
…w resolves with an app instance. Fixed contextHandler type
|
||
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. |
There was a problem hiding this comment.
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
There was a problem hiding this 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 withappInstanceId
.
- This might be resolved through documentation and naming conventions (e.g. replacing
- The return types of
open
andraiseIntent
are inconsistent.open
returns aPromise<AppInstance | null>
whileraiseIntent
returns anIntentResolution
containing asource
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 renamedTargetApp
(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 ofInstanceMetadata
or anAppMetadata
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.
- further refine the
- The proposed method of responding to an app that has called
addContextListener
on an AppInstance (retrieving that app's ownAppInstance
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 theopen
issues, an app would have to register a listener for subscriptions and then retain Ids orAppInstance
objects to use for this purpose. - In the case of
fdc3.raiseIntent
theContextHandler
argument onaddIntentListener
would need to have an additional argument added to it to receive the source instanceId. - This feels far too complex to be practical.
- In the case of
- As this is the primary usecase being addressed this is perhaps the most important issue to address in the proposal.
- The responding app currently has no way to get the
|
||
Returns the instance of an app for a given identifier. | ||
|
||
An instanceId can be obtained from |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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).
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 |
@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. |
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:
IntentResolution
source properties can be used to obtain an AppInstanceContextHandler
functions now get an additional source arg that provides a reference to the instance sending the context or intent