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

Async listener and private channel events #1305

Merged
merged 15 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

* Added clarification that `id` field values SHOULD always be strings to context schema definition (a restriction that can't easily be represented in the generated types). ([#1149](https://github.com/finos/FDC3/pull/1149))
* Added requirement that Standard versions SHOULD avoid the use unions in context and API definitions wherever possible as these can be hard to replicate and MUST avoid unions of primitive types as these can be impossible to replicate in other languages. ([#120](https://github.com/finos/FDC3/pull/1200))
* Added `addEventListener` to the `DesktopAgent` API to provide support for event listener for non-context and non-intent events, including a USER_CHANNEL_CHANGED event ([#1207](https://github.com/finos/FDC3/pull/1207))
* Added an `async` `addEventListener` function to the `PrivateChannel` API to replace the deprecated, synchronous `onAddContextListener`, `onUnsubscribe` and `onDisconnect` functions and to keep consistency with the DesktopAgent API. ([#1305](https://github.com/finos/FDC3/pull/1305))

### Changed

* `Listener.unsubscribe()` was made async (the return type was changed from `void` to `Promise<void>`) for consistency with the rest of the API. ([#1305](https://github.com/finos/FDC3/pull/1305))

### Deprecated

* Made `IntentMetadata.displayName` optional as it is deprecated. ([#1280](https://github.com/finos/FDC3/pull/1280))
* Deprecated `PrivateChannel`'s synchronous `onAddContextListener`, `onUnsubscribe` and `onDisconnect` functions in favour of an `async` `addEventListener` function consistent with the one added to `DesktopAgent` in #1207. ([#1305](https://github.com/finos/FDC3/pull/1305))

### Fixed

Expand Down
99 changes: 91 additions & 8 deletions docs/api/ref/Events.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,35 @@
title: Events
---

In addition to intent and context events, the FDC3 API may be used to listen for other types of events via the `addEventListener()` function.
In addition to intent and context events, the FDC3 API and PrivateChannel API may be used to listen for other types of events via their `addEventListener()` functions.

## `EventHandler`

```ts
type EventHandler = (event: FDC3Event) => void;
type EventHandler = (event: FDC3Event | PrivateChannelEvent) => void;
```

Describes a callback that handles non-context and non-intent events. Provides the details of the event.
Describes a callback that handles non-context and non-intent events. Provides the details of the event.

Used when attaching listeners to events.
Used when attaching listeners to events.

**See also:**

- [`DesktopAgent.addEventListener`](DesktopAgent#addEventListener)
- [`FDC3Event`](#fdc3event)

## `FDC3EventType`

```ts
enum FDC3EventType {
USER_CHANNEL_CHANGED = "USER_CHANNEL_CHANGED"
}
```

Enumeration defining the types of (non-context and non-intent) events that may be received via the FDC3 API's `addEventListener` function.
Enumeration defining the types of (non-context and non-intent) events that may be received via the FDC3 API's `addEventListener` function.

**See also:**

- [`DesktopAgent.addEventListener`](DesktopAgent#addEventListener)

## `FDC3Event`
Expand All @@ -39,14 +42,17 @@ interface FDC3Event {
}
```

Type representing the format of event objects that may be received via the FDC3 API's `addEventListener` function. Will always include both `type` and `details`, which describe type of the event and any additional details respectively.
Type representing the format of event objects that may be received via the FDC3 API's `addEventListener` function.

Events will always include both `type` and `details` properties, which describe the type of the event and any additional details respectively.

**See also:**

- [`DesktopAgent.addEventListener`](DesktopAgent#addEventListener)
- [`FDC3EventType`](#fdc3eventtype)


### `FDC3ChannelChangedEvent`

```ts
interface FDC3ChannelChangedEvent extends FDC3Event {
readonly type: FDC3EventType.USER_CHANNEL_CHANGED;
Expand All @@ -56,4 +62,81 @@ interface FDC3ChannelChangedEvent extends FDC3Event {
}
```

Type representing the format of USER_CHANNEL_CHANGED events. The identity of the channel joined is provided as `details.currentChannelId`, which will be `null` if the app is no longer joined to any channel.
Type representing the format of USER_CHANNEL_CHANGED events.

The identity of the channel joined is provided as `details.currentChannelId`, which will be `null` if the app is no longer joined to any channel.

## `PrivateChannelEventType`

```ts
enum PrivateChannelEventType {
ADD_CONTEXT_LISTENER = "addContextListener",
UNSUBSCRIBE = "unsubscribe",
DISCONNECT = "disconnect"
}
```

Enumeration defining the types of (non-context and non-intent) events that may be received via a PrivateChannel's `addEventListener` function.

**See also:**

- [`PrivateChannel.addEventListener`](PrivateChannel#addEventListener)

## `PrivateChannelEvent`

```ts
interface PrivateChannelEvent {
readonly type: PrivateChannelEventType;
readonly details: any;
}
```

Type defining the format of event objects that may be received via a PrivateChannel's `addEventListener` function.

**See also:**

- [`PrivateChannel.addEventListener`](PrivateChannel#addEventListener)
- [`PrivateChannelEventType`](#privatechanneleventtype)

### `PrivateChannelAddContextListenerEvent`

```ts
interface PrivateChannelAddContextListenerEvent extends PrivateChannelEvent {
readonly type: PrivateChannelEventType.ADD_CONTEXT_LISTENER;
readonly details: {
contextType: string | null
};
}
```

Type defining the format of events representing a context listener being added to the channel (`addContextListener`). Desktop Agents MUST fire this event for each invocation of `addContextListener` on the channel, including those that occurred before this handler was registered (to prevent race conditions).

The context type of the listener added is provided as `details.contextType`, which will be `null` if all event types are being listened to.

### `PrivateChannelUnsubscribeEvent`

```ts
interface PrivateChannelUnsubscribeEvent extends PrivateChannelEvent {
readonly type: PrivateChannelEventType.UNSUBSCRIBE;
readonly details: {
contextType: string | null
};
}
```

Type defining the format of events representing a context listener removed from the channel (`Listener.unsubscribe()`). Desktop Agents MUST call this when `disconnect()` is called by the other party, for each listener that they had added.

The context type of the listener removed is provided as `details.contextType`, which will be `null` if all event types were being listened to.

### `PrivateChannelDisconnectEvent`

```ts
export interface PrivateChannelDisconnectEvent extends PrivateChannelEvent {
readonly type: PrivateChannelEventType.DISCONNECT;
readonly details: null | undefined;
}
```

Type defining the format of events representing a remote app being terminated or is otherwise disconnecting from the PrivateChannel. This event is fired in addition to unsubscribe events that will also be fired for any context listeners the disconnecting app had added.

No details are provided.
138 changes: 84 additions & 54 deletions docs/api/ref/PrivateChannel.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@ Object representing a private context channel, which is intended to support secu
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 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
// functions
addEventListener(type: PrivateChannelEventType | null, handler: EventHandler): Promise<Listener>;
disconnect(): Promise<void>;

//deprecated functions
onAddContextListener(handler: (contextType?: string) => void): Listener;
onUnsubscribe(handler: (contextType?: string) => void): Listener;
onDisconnect(handler: () => void): Listener;
disconnect(): void;
}
```

Expand All @@ -36,34 +39,43 @@ interface PrivateChannel extends Channel {

### '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 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 `addEventListener()` 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).

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
```ts
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});
});
});
const addContextListener = channel.addEventListener("addContextListener",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bingenito need to replicate switch to new function in .NET version below

(event: PrivateChannelAddContextListenerEvent) => {
console.log(`remote side added a listener for ${event.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 unsubscribeListener = channel.onUnsubscribe((contextType) => {
feed.stop(symbol);
});
const unsubscribeListener = channel.addEventListener("unsubscribe",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bingenito need to replicate switch to new function in .NET version below

(event: PrivateChannelUnsubscribeEvent) => {
console.log(`remote side unsubscribed a listener for ${event.contextType}`);
feed.stop(symbol);
}
);

// This gets called if the remote side closes
const disconnectListener = channel.onDisconnect(() => {
feed.stop(symbol);
});
const disconnectListener = channel.addEventListener("disconnect",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bingenito need to replicate switch to new function in .NET version below

() => {
console.log(`remote side disconnected`);
feed.stop(symbol);
}
);

return channel;
});
Expand All @@ -75,88 +87,106 @@ The 'client' application retrieves a `Channel` by raising an intent with context

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
```ts
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");
});
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.type == "private") {
result.addEventListener("disconnect", () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bingenito need to replicate switch to new function in .NET version below

console.warn("Quote feed went down");
});

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

## Methods
## Functions

### `onAddContextListener`
### `addEventListener`

```ts
onAddContextListener(handler: (contextType?: string) => void): Listener;
addEventListener(type: PrivateChannelEventType | null, handler: EventHandler): Promise<Listener>;
```

Adds a listener that will be called each time that the remote app invokes addContextListener on this channel.
Register a handler for events from the PrivateChannel. Whenever the handler function is called it will be passed an event object with details related to the event.

```ts
// any event type
const listener: Listener = await myPrivateChannel.addEventListener(null,
(event: PrivateChannelEvent) => {
console.log(`Received event ${event.type}\n\tDetails: ${event.details}`);
}
);

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).
// listener for a specific event type
const channelChangedListener: Listener = await myPrivateChannel.addEventListener(
PrivateChannelEventType.ADD_CONTEXT_LISTENER,
(event: PrivateChannelAddContextListenerEvent) => { ... }
);
```

**See also:**

- [`Channel.addContextListener`](Channel#addcontextlistener)
- [`PrivateChannelEventType`](./Events#privatechanneleventtype)
- [`EventHandler`](./Events#eventhandler)

### `onUnsubscribe`
### `disconnect`

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

Adds a listener that will be called whenever the remote app invokes `Listener.unsubscribe()` on a context listener that it previously added.
May be called to indicate that a participant will no longer interact with this channel.

Desktop Agents MUST call this when disconnect() is called by the other party, for each listener that they had added.
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 event handler for `unsubscribe` events added by the other party to be called) before triggering any handlers for `disconnect` events added by the other party.

**See also:**

- [`Listener`](Types#listener)

### `onDisconnect`
## Deprecated Functions

### `onAddContextListener`

```ts
onDisconnect(handler: () => void): Listener;
onAddContextListener(handler: (contextType?: string) => 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:**
Deprecated in favour of the async `addEventListener("addContextListener", handler)` function.

- [`disconnect`](#disconnect)
Adds a listener that will be called each time that the remote app invokes addContextListener on this channel.

### `disconnect`
### `onUnsubscribe`

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

May be called to indicate that a participant will no longer interact with this channel.
Deprecated in favour of the async `addEventListener("unsubscribe", handler)` function.

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.
Adds a listener that will be called whenever the remote app invokes `Listener.unsubscribe()` on a context listener that it previously added.

**See also:**
### `onDisconnect`

- [`onUnsubscribe`](#onunsubscribe)
- [`Listener`](Types#listener)
```ts
onDisconnect(handler: () => void): Listener;
```

Deprecated in favour of the aysnc `addEventListener("disconnect", handler)` function.
bingenito marked this conversation as resolved.
Show resolved Hide resolved

Adds a listener that will be called when the remote app terminates, for example when its window is closed or because disconnect was called.
Loading