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

433 Feeds: PrivateChannels and returning channels from intents #508

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 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
c6428ba
Merge branch 'master' into 433-private-channels-returned-by-intents
kriswest Feb 21, 2022
a3d9b40
correcting a test
kriswest Feb 21, 2022
0aeef0b
Merge branch 'master' into 433-private-channels-returned-by-intents
kriswest Feb 28, 2022
927e9e7
Apply suggestions from code review
kriswest Mar 1, 2022
3fce359
Apply suggestions from code review
kriswest Mar 1, 2022
1e139be
Apply suggestions from code review
kriswest Mar 1, 2022
189faf8
Apply suggestions from code review
kriswest Mar 1, 2022
5e90753
Apply suggestions from code review
kriswest Mar 1, 2022
83f4a6d
Apply suggestions from code review
kriswest Mar 7, 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 (Use
public readonly type: string;
```

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

### `displayMetadata`

Expand Down
242 changes: 189 additions & 53 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',
Copy link

@pbaize pbaize Nov 30, 2021

Choose a reason for hiding this comment

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

Should this be an error or should getResult just return Promise<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.

I suspect that, if a result was expected, the promise rejection is (slightly) more useful as it can be handled with other errors, rather than requiring a check for an undefined result. However, it works as a pattern either way.

IntentHandlerRejected = 'IntentHandlerRejected',
}
```
Expand Down
118 changes: 63 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,7 +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.
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 @@ -75,53 +81,40 @@ 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: `#FF0000`. May be any color value supported by CSS, e.g. name, hex, rgba, etc..
*/
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

#### See also
* [`Channel`](Channel)
* [`DesktopAgent.getUserChannels`](DesktopAgent#getuserchannels)

## `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 @@ -134,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 @@ -149,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 @@ -165,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 @@ -192,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 (result){
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
151 changes: 151 additions & 0 deletions docs/api/ref/PrivateChannel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
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 `PrivateChannels` 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).

kriswest marked this conversation as resolved.
Show resolved Hide resolved
Although this interaction occurs entirely in frontend code, we refer to it as the 'server-side' interaction as it receives a request and initiates a stream of responses.
```typescript
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:
The 'client' application retrieves a `Channel` by raising an intent with context and awaiting the result. It adds a `ContextListener` so that it can receive messages from it. If a `PrivateChannel` was returned this may in turn trigger a handler added on the 'server-side' with `onAddContextListener()` and start the stream. A listener may also be to clear up if the 'server-side' disconnects from the stream.

Although this interaction occurs entirely in frontend code, we refer to it as the 'client-side' interaction as it requests and receives a stream of responses.

```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 it's a channel
if (result && result.addContextListener) {
const listener = result.addContextListener("price", (quote) => console.log(quote));

//if it's a PrivateChannel
if (result.onDisconnect) {
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 invocation 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 listener 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 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