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

498 Allow intents to be resolved on output type (where they return data) #499

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
167a7d4
Merge branch '432-return-data-from-an-intent' into 498-resolve-intent…
kriswest Nov 12, 2021
85619ec
Add suport for metadata on output types for intent to appD and the fi…
kriswest Nov 12, 2021
0adfd83
changelog
kriswest Nov 12, 2021
bd97624
WIP
kriswest Nov 18, 2021
ea69042
WIP
kriswest Nov 18, 2021
22cd3be
Merge branch '432-return-data-from-an-intent' into 498-resolve-intent…
kriswest Nov 18, 2021
6d68022
Changing IntentResolution.getData() to IntentResolution.getResult()
kriswest Nov 18, 2021
bb1e120
Changing IntentResolution.getData() to IntentResolution.getResult()
kriswest Nov 18, 2021
e3a962e
WIP
kriswest Nov 18, 2021
240b424
WIP
kriswest Nov 19, 2021
9b0b3b4
completed draft of feeds
kriswest Nov 19, 2021
075067a
changelog
kriswest Nov 19, 2021
ea3e2eb
outputContext -> resultContext and other comments from review
kriswest Nov 22, 2021
157a46f
Mergeing updates from review of upstream PR
kriswest Nov 22, 2021
4c274a4
Apply suggestions from code review
kriswest Dec 13, 2021
461bf95
Merge branch 'master' into 498-resolve-intents-on-output-type
kriswest Jan 25, 2022
3fa81cb
Merge branch '498-resolve-intents-on-output-type' into 433-private-ch…
kriswest Jan 25, 2022
dbd3e81
Merge branch 'master' into 433-private-channels-returned-by-intents
kriswest Feb 4, 2022
ef1d315
Merge branch 'master' into 433-private-channels-returned-by-intents
kriswest Feb 21, 2022
6412fba
Removing defunct paragraph from docs/api/spec.md (bad merge)
kriswest Feb 21, 2022
1da923a
prettier
kriswest Feb 21, 2022
b88b4af
Update docs/api/ref/DesktopAgent.md
kriswest Feb 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
* Added [**FDC3 Workbench**](https://fdc3.finos.org/toolbox/fdc3-workbench/), an FDC3 API developer application ([#457](https://github.com/finos/FDC3/pull/457))
* Added advice on how to `broadcast` complex context types, composed of other types, so that other apps can listen for both the complex type and simpler constituent types ([#464](https://github.com/finos/FDC3/pull/464))
* Added the ability to return data from an intent, via the addition of an IntentHandler type and a `getResult()` to IntentResolution, both of which return a Promise of a Context object. ([#495](https://github.com/finos/FDC3/pull/495))
* Added a field to specify the Context type that intent can return to the AppD Application schema and extended the findIntent API calls to be able to use it for resolution. ([#499](https://github.com/finos/FDC3/pull/499))
* Added the ability to return a Channel from an intent (via the `IntentResult` type), resolver support for intents that return Channels and the concept of PrivateChannels. ([#508](https://github.com/finos/FDC3/pull/508))
* Added error `UserCancelled` to the `ResolveError` enumeration to be used when user closes the resolver UI or otherwise cancels resolution of a raised intent ([#522 ](https://github.com/finos/FDC3/pull/522))
* Added an `instanceId` (and optional `instanceMetadata`) field to `AppMetadata` allowing it to refer to specific app instances and thereby supporting targetting of intents to specific app instances. Also added a `findInstanes()` function to the desktop agent. ([#509]((https://github.com/finos/FDC3/pull/509))
* Added a References and Bibliography section to the Standard's documentation to hold links to 'normative references' and other documentation that is useful for understanding the standard ([#530](https://github.com/finos/FDC3/pull/530))
Expand Down
2 changes: 1 addition & 1 deletion docs/api/ref/Channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Uniquely identifies the channel. It is either assigned by the desktop agent (sys
public readonly type: string;
```

Can be _system_ or _app_.
Can be _system_, _app_ or _private_.

### `displayMetadata`

Expand Down
239 changes: 187 additions & 52 deletions docs/api/ref/DesktopAgent.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/api/ref/Errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ Contains constants representing the errors that can be encountered when calling
* [`DesktopAgent.raiseIntent`](DesktopAgent#raiseintent)
* [`DesktopAgent.raiseIntentForContext`](DesktopAgent#raiseintentforcontext)

## `DataError`
## `ResultError`

```typescript
enum DataError {
NoDataReturned = 'NoDataReturned',
enum ResultError {
NoResultReturned = 'NoResultReturned',
IntentHandlerRejected = 'IntentHandlerRejected',
Copy link
Contributor

Choose a reason for hiding this comment

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

We may need some more discussion around error handling. For instance, a request for a quote would likely return some sort of well-defined "Quote" context, but what if the end user is not authorized to receive that market data? We wouldn't want to muddy the Quote context with arbitrary error messages, but then the DataError enum type provides no information about why the intent was rejected, which in this case would result in quite a bit of potential confusion for the end user.

We may want to either (a) consider allowing at least a simple error string to be rejected/thrown, or (b) allow for the possibility of more than one context being returned (e.g. an Error context. Receiving apps may then disambiguate by examining the type member).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At present, an app resolving an intent has no means to identify the app raising the intent and couldn't assess authorization. Hence, any such mechanism would have to be a function of the Desktop Agent. However, I take the point on returning some info about why the promise was rejected (for feedback to the end-user - 'IntentHandlerRejected' is not all that informative and we are deliberately losing any info on why the rejection occurred).

I don't particularly like the idea of using an 'ErrorContext' type (I think that overloads the purpose of context). However, we could use the javascript Error type (or similar that we define), setting the message to these values and setting the optional cause property to the error/rejection message returned by the intent handler... However, its a departure from all other rejects, which reject with just a string.

}
```
Expand Down
117 changes: 62 additions & 55 deletions docs/api/ref/Metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ FDC3 API operations return various types of metadata.

```ts
interface AppIntent {
intent: IntentMetadata;
apps: Array<AppMetadata>;
readonly intent: IntentMetadata;
readonly apps: Array<AppMetadata>;
}
```
An interface that represents the binding of an intent to apps, returned as part of intent disocvery.
Expand Down Expand Up @@ -46,14 +46,20 @@ interface AppMetadata {
/** A tooltip for the application that can be used to render UI elements */
readonly tooltip?: string;

/** A longer, multi-paragraph description for the application that could include markup */
/** A longer, multi-paragraph description for the application that could include mark-up */
readonly description?: string;

/** A list of icon URLs for the application that can be used to render UI elements */
readonly icons?: Array<Icon>;

/** A list of image URLs for the application that can be used to render UI elements */
readonly images?: Array<string>;

/** The type of result returned for any intent specified during resolution.
* May express a particular context type (e.g. "fdc3.instrument"), channel
* (e.g. "channel") or a channel that will receive a specified type
* (e.g. "channel<fdc3.instrument>"). */
readonly resultType?: string | null;
}
```

Expand All @@ -63,9 +69,7 @@ Will always includes at least a `name` property, which can be used with [`open`]

Optionally, extra information from the app directory can be returned, to aid in rendering UI elements, e.g. a context menu. This includes a title, description, tooltip and icon and image URLs.

In situations where a desktop agent connects to multiple app directories or multiple versions of the same app exists in a single app directory, it may be neccessary to specify appId and version to target applications that share the same name.

`AppMetadata`
In situations where a desktop agent connects to multiple app directories or multiple versions of the same app exists in a single app directory, it may be necessary to specify `appId` or `version` to target applications that share the same name.

#### See also
* [`AppIntent.apps`](AppIntent)
Expand All @@ -76,53 +80,41 @@ In situations where a desktop agent connects to multiple app directories or mult
## `DisplayMetadata`

```ts
public interface DisplayMetadata {
name?: string;
color?: string;
glyph?: string;
interface DisplayMetadata {
/**
* A user-readable name for this channel, e.g: `"Red"`
*/
readonly name?: string;
/**
* The color that should be associated within this channel when displaying this channel in a UI, e.g: `0xFF0000`.
*/
readonly color?: string;
/**
* A URL of an image that can be used to display this channel
*/
readonly glyph?: string;
}
```

A desktop agent (typically for _system_ channels) may want to provide additional information about how a channel can be represented in a UI. A common use case is for color linking.

#### Properties

#### `name`

```ts
name?: string;
```

The display name for the channel.

#### `color`

```ts
color?: string;
```

A name, hex, rgba, etc. that should be associated within the channel when displaying it in a UI.

#### `glyph`

```ts
glyph: string;
```

A URL of an image that can be used to display this channel.

#### See also

* [`Channel`](Channel)
* [`DesktopAgent.getSystemChannels`](DesktopAgent#getsystemchannels)

## `ImplementationMetadata`

```typescript
public interface ImplementationMetadata {
fdc3Version: string;
provider: string;
providerVersion?: string;
```ts
interface ImplementationMetadata {
/** The version number of the FDC3 specification that the implementation provides.
* The string must be a numeric semver version, e.g. 1.2 or 1.2.1.
*/
readonly fdc3Version: string;
/** The name of the provider of the FDC3 Desktop Agent Implementation (e.g. Finsemble, Glue42, OpenFin etc.). */
readonly provider: string;
/** The version of the provider of the FDC3 Desktop Agent Implementation (e.g. 5.3.0). */
readonly providerVersion?: string;
}
```

Expand All @@ -135,8 +127,10 @@ Metadata relating to the FDC3 [DesktopAgent](DesktopAgent) object and its provid

```ts
interface IntentMetadata {
name: string;
displayName: string;
/** The unique name of the intent that can be invoked by the raiseIntent call */
readonly name: string;
/** A friendly display name for the intent that should be used to render UI elements */
readonly displayName: string;
}
```

Expand All @@ -150,6 +144,7 @@ The interface used to describe an intent within the platform.

```ts
interface IntentResolution {

/**
* Metadata about the app instance that was selected (or started) to resolve the intent.
* `source.instanceId` MUST be set, indicating the specific app instance that
Expand All @@ -166,15 +161,21 @@ interface IntentResolution {
*/
readonly version?: string;
/**
* Retrieves a promise that will resolve to data returned by the
* application that resolves the raised intent. If an error occurs
* (i.e. an error is thrown by the handler function, the promise
* returned by the handler function is rejected, or no promise is
* returned) then the Desktop Agent MUST reject the promise
* returned by the `getResult()` function of the `IntentResolution`
* with a string from the `DataError` enumeration.
* Retrieves a promise that will resolve to either `Context` data returned
* by the application that resolves the raised intent or a `Channel`
* established and returned by the app resolving the intent.
*
* A `Channel` returned MAY be of the `PrivateChannel` type. The
* client can then `addContextListener()` on that channel to, for example,
* receive a stream of data.
*
* If an error occurs (i.e. an error is thrown by the handler function,
* the promise it returns is rejected, or a promise is not returned by the
* handler function) then the Desktop Agent MUST reject the promise returned
* by the `getResult()` function of the `IntentResolution` with a string from
* the `ResultError` enumeration.
*/
getResult(): Promise<Context>;
getResult(): Promise<IntentResult>;
}
```

Expand All @@ -193,13 +194,19 @@ try {
//some time later
await agent.raiseIntent("UpdateOrder", context, resolution.source);
}
catch (err) { ... }

//resolve a "Client-Service" type intent with a data response
catch (err) { ... }
//resolve a "Client-Service" type intent with a data or channel response
let resolution = await agent.raiseIntent("intentName", context);
try {
const result = await resolution.getResult();
console.log(`${resolution.source} returned ${JSON.stringify(result)}`);
if (result && result.broadcast) { //detect whether the result is Context or a Channel
console.log(`${resolution.source} returned a channel with id ${result.id}`);
} else if (){
console.log(`${resolution.source} returned data: ${JSON.stringify(result)}`);
} else {
console.error(`${resolution.source} didn't return anything`
}
} catch(error) {
console.error(`${resolution.source} returned an error: ${error}`);
}
Expand Down
144 changes: 144 additions & 0 deletions docs/api/ref/PrivateChannel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
---
id: PrivateChannel
sidebar_label: PrivateChannel
title: PrivateChannel
hide_title: true
---
# `PrivateChannel`

Object representing a private context channel, which is intended to support secure communication between applications, and extends the Channel interface with event handlers which provide information on the connection state of both parties, ensuring that desktop agents do not need to queue or retain messages that are broadcast before a context listener is added and that applications are able to stop broadcasting messages when the other party has disconnected.

It is intended that Desktop Agent implementations:
- SHOULD restrict external apps from listening or publishing on this channel.
- MUST prevent private channels from being retrieved via fdc3.getOrCreateChannel.
- MUST provide the `id` value for the channel as required by the Channel interface.

```ts
interface PrivateChannel extends Channel {
// methods
onAddContextListener(handler: (contextType?: string) => void): Listener;
onUnsubscribe(handler: (contextType?: string) => void): Listener;
onDisconnect(handler: () => void): Listener;
disconnect(): void;
}
```

#### See also

* [`Channel`](Channel)
* [`Listener`](Types#listener)
* [`DesktopAgent.addIntentListener`](DesktopAgent#addintentlistener)
* [`DesktopAgent.createPrivateChannel`](DesktopAgent#createPrivateChannel)
* [`DesktopAgent.raiseIntent`](DesktopAgent#raiseintent)

## Examples
### 'Server-side' example:
The intent app establishes and returns a PrivateChannel to the client (who is awaiting `getResult()`). When the client calls `addContextlistener()` on that channel, the intent app receives notice via the handler added with `onAddContextListener()` and knows that the client is ready to start receiving quotes.

The Desktop Agent knows that a channel is being returned by inspecting the object returned from the handler (e.g. check constructor or look for private member).

```javascript
fdc3.addIntentListener("QuoteStream", async (context) => {
const channel: PrivateChannel = await fdc3.createPrivateChannel();
const symbol = context.id.ticker;

// This gets called when the remote side adds a context listener
const addContextListener = channel.onAddContextListener((contextType) => {
// broadcast price quotes as they come in from our quote feed
feed.onQuote(symbol, (price) => {
channel.broadcast({ type: "price", price});
});
});

// This gets called when the remote side calls Listener.unsubscribe()
const unsubscriberListener = channel.onUnsubscribe((contextType) => {
feed.stop(symbol);
});

// This gets called if the remote side closes
const disconnectListener = channel.onDisconnect(() => {
feed.stop(symbol);
});

return channel;
});
```

### 'Client-side' example:

```javascript
try {
const resolution3 = await fdc3.raiseIntent("QuoteStream", { type: "fdc3.instrument", id : { symbol: "AAPL"}});
try {
const result = await resolution3.getResult();
//check that we got a result and that its a channel
if (result && result.addContextListener) {
const listener = result.addContextListener("price", (quote) => console.log(quote));
result.onDisconnect(() => {
console.warn("Quote feed went down");
});

// Sometime later...
listener.unsubscribe();
} else {
console.warn(`${resolution3.source} did not return a channel`);
}
} catch(channelError){
console.log(`Error: ${resolution3.source} returned an error: ${channelError}`);
}
} catch (resolverError) {
console.error(`Error: Intent was not resolved: ${resolverError}`);
}
```


## Methods

### `onAddContextListener`
```ts
onAddContextListener(handler: (contextType?: string) => void): Listener;
```
Adds a listener that will be called each time that the remote app invokes addContextListener on this channel.

Desktop Agents MUST call this for each invokation of addContextListener on this channel, including those that occurred before this handler was registered (to prevent race conditions).

#### See also
* [`Channel.addContextListener`](Channel#addcontextlistener)

### `onUnsubscribe`

```ts
onUnsubscribe(handler: (contextType?: string) => void): Listener;
```

Adds a listener that will be called whenever the remote app invokes `Listener.unsubscribe()` on a context listener that it previously added.

Desktop Agents MUST call this when disconnect() is called by the other party, for each listner that they had added.

#### See also
* [`Listener`](Types#listener)

### `onDisconnect`

```ts
onDisconnect(handler: () => void): Listener;
```

Adds a listener that will be called when the remote app terminates, for example when its window is closed or because disconnect was called. This is in addition to calls that will be made to an onUnsubscribe listeners.

#### See also
* [`disconnect`](#disconnect)

### `disconnect`

```ts
disconnect(): void;
```

May be called to indicate that a participant will no longer interact with this channel.

After this function has been called, Desktop Agents SHOULD prevent apps from broadcasting on this channel and MUST automatically call Listener.unsubscribe() for each listener that they've added (causing any `onUnsubscribe` handler added by the other party to be called) before triggering any onDisconnect handler added by the other party.

#### See also
* [`onUnsubscribe`](#onunsubscribe)
* [`Listener`](Types#listener)
Loading