diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d45a4f67..9f50d4860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ 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 `userChannelChanged` 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)) * Added reference materials and supported platforms information for FDC3 in .NET via the [finos/fdc3-dotnet](https://github.com/finos/fdc3-dotnet) project. ([#1108](https://github.com/finos/FDC3/pull/1108)) * Specifications for getAgent() and Browser-Resident Desktop Agents. * Specification for Preload Desktop Agents. This content was previously in the supported platforms section. It had been revised and amended to include recommended behavior related to the new validateAppIdentity() function. @@ -20,12 +22,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed +* `Listener.unsubscribe()` was made async (the return type was changed from `void` to `Promise`) for consistency with the rest of the API. ([#1305](https://github.com/finos/FDC3/pull/1305)) +* Added reference materials and supported platforms information for FDC3 in .NET via the [finos/fdc3-dotnet](https://github.com/finos/fdc3-dotnet) project. ([#1108](https://github.com/finos/FDC3/pull/1108)) * The supported platforms page in the FDC3 documentation was moved into the API section as the information it provides all relates to FDC3 Desktop Agent API implementations. ([#1108](https://github.com/finos/FDC3/pull/1108)) * FDC3 apps are now encouraged to instantiate their FDC3 interface (DesktopAgent) using the `getAgent()` function provided by the `@finos/fdc3` module. This will allow apps to interoperate in either traditional Preload DAs (i.e. Electron) as well as the new Browser-Resident DAs. ### 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 diff --git a/docs/api/ref/DesktopAgent.md b/docs/api/ref/DesktopAgent.md index 9c452b1f2..4388c8055 100644 --- a/docs/api/ref/DesktopAgent.md +++ b/docs/api/ref/DesktopAgent.md @@ -48,7 +48,7 @@ interface DesktopAgent { leaveCurrentChannel() : Promise; // non-context events - addEventListener(type: FDC3EventType | null, handler: EventHandler): Promise; + addEventListener(type: FDC3EventTypes | null, handler: EventHandler): Promise; //implementation info getInfo(): Promise; @@ -183,7 +183,7 @@ var contactListener = await _desktopAgent.AddContextListener("fdc3.cont ```ts -addEventListener(type: FDC3EventType | null, handler: EventHandler): Promise; +addEventListener(type: FDC3EventTypes | null, handler: EventHandler): Promise; ``` @@ -210,12 +210,11 @@ Whenever the handler function is called it will be passed an event object with d const listener = await fdc3.addEventListener(null, event => { ... }); // listener for a specific event type that logs its details -const userChannelChangedListener = await fdc3.addEventListener(FDC3EventType.USER_CHANNEL_CHANGED, event => { +const userChannelChangedListener = await fdc3.addEventListener("userChannelChanged ", event => { console.log(`Received event ${event.type}\n\tDetails: ${event.details}`); //do something else with the event }); ``` - @@ -226,6 +225,12 @@ Not implemented +**See also:** + +- [`FDC3EventTypes`](./Events#fdc3eventtypes) +- [`FDC3Event`](./Events#fdc3event) +- [`EventHandler`](./Events#eventhandler) + ### `addIntentListener` diff --git a/docs/api/ref/Events.md b/docs/api/ref/Events.md index c8692018b..3eb89230c 100644 --- a/docs/api/ref/Events.md +++ b/docs/api/ref/Events.md @@ -2,58 +2,152 @@ 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. + +## `ApiEvent` + +Type defining a basic event object that may be emitted by an FDC3 API interface such as DesktopAgent or PrivateChannel. There are more specific event types defined for each interface. + +```ts +interface ApiEvent { + readonly type: string; + readonly details: any; +} +``` + +**See also:** + +- [`FDC3Event`](#fdc3event) +- [`PrivateChannelEvent`](#privatechannelevent) ## `EventHandler` ```ts -type EventHandler = (event: FDC3Event) => void; +type EventHandler = (event: ApiEvent) => 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` +- [`DesktopAgent.addEventListener`](DesktopAgent#addeventlistener) +- [`PrivateChannel.addEventListener`](PrivateChannel#addeventlistener) +- [`ApiEvent`](#apievent) + +## `FDC3EventTypes` + ```ts -enum FDC3EventType { - USER_CHANNEL_CHANGED = "USER_CHANNEL_CHANGED" -} +type FDC3EventTypes = "userChannelChanged"; ``` -Enumeration defining the types of (non-context and non-intent) events that may be received via the FDC3 API's `addEventListener` function. +Type defining valid type strings for DesktopAgent interface events. **See also:** -- [`DesktopAgent.addEventListener`](DesktopAgent#addEventListener) + +- [`DesktopAgent.addEventListener`](DesktopAgent#addeventlistener) ## `FDC3Event` ```ts -interface FDC3Event { - readonly type: FDC3EventType; +interface FDC3Event extends ApiEvent{ + readonly type: FDC3EventTypes; readonly details: any; } ``` -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) +- [`DesktopAgent.addEventListener`](DesktopAgent#addeventlistener) +- [`FDC3EventTypes`](#fdc3eventtypes) ### `FDC3ChannelChangedEvent` + ```ts interface FDC3ChannelChangedEvent extends FDC3Event { - readonly type: FDC3EventType.USER_CHANNEL_CHANGED; + readonly type: "userChannelChanged"; readonly details: { currentChannelId: string | null }; } ``` -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. \ No newline at end of file +Type representing the format of `userChannelChanged` 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. + +## `PrivateChannelEventTypes` + +```ts +type PrivateChannelEventTypes = "addContextListener" | "unsubscribe" | "disconnect"; +``` + +Type defining valid type strings for Private Channel events. + +**See also:** + +- [`PrivateChannel.addEventListener`](PrivateChannel#addeventlistener) + +## `PrivateChannelEvent` + +```ts +interface PrivateChannelEvent extends ApiEvent { + readonly type: PrivateChannelEventTypes; + 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) +- [`PrivateChannelEventTypes`](#privatechanneleventtypes) + +### `PrivateChannelAddContextListenerEvent` + +```ts +interface PrivateChannelAddContextListenerEvent extends PrivateChannelEvent { + readonly type: "addContextListener"; + 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: "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: "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. diff --git a/docs/api/ref/PrivateChannel.md b/docs/api/ref/PrivateChannel.md index 2076f0e08..aa8692016 100644 --- a/docs/api/ref/PrivateChannel.md +++ b/docs/api/ref/PrivateChannel.md @@ -15,7 +15,7 @@ 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. @@ -23,11 +23,14 @@ It is intended that Desktop Agent implementations: ```ts interface PrivateChannel extends Channel { - // methods + // functions + addEventListener(type: PrivateChannelEventTypes | null, handler: EventHandler): Promise; + disconnect(): Promise; + + //deprecated functions onAddContextListener(handler: (contextType?: string) => void): Listener; onUnsubscribe(handler: (contextType?: string) => void): Listener; onDisconnect(handler: () => void): Listener; - disconnect(): void; } ``` @@ -59,7 +62,7 @@ interface IPrivateChannel : IChannel, IIntentResult ### '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. If the client unsubscribes it's context listener or disconnects from the channel, the intent app may receive notice via the the `onUnsubscribe()` and `onDisconnect()` and can stop sending data and clean-up. +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). @@ -74,22 +77,31 @@ fdc3.addIntentListener("QuoteStream", async (context) => { 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", + (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", + (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", + () => { + console.log(`remote side disconnected`); + feed.stop(symbol); + } + ); return channel; }); @@ -141,23 +153,24 @@ Although this interaction occurs entirely in frontend code, we refer to it as th 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", () => { + 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}`); } catch(resultError: ResultError) { console.log(`Error: ${resolution3.source} returned an error: ${resultError}`); } @@ -205,107 +218,148 @@ catch (Exception ex) -## Methods +## Functions -### `onAddContextListener` +### `addEventListener` ```ts -onAddContextListener(handler: (contextType?: string) => void): Listener; +addEventListener(type: PrivateChannelEventTypes | null, handler: EventHandler): Promise; ``` ```csharp -IListener OnAddContextListener(Action handler); +Not implemented ``` -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). +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. -**See also:** + + -- [`Channel.addContextListener`](Channel#addcontextlistener) +```ts +// any event type +const listener: Listener = await myPrivateChannel.addEventListener(null, + (event: PrivateChannelEvent) => { + console.log(`Received event ${event.type}\n\tDetails: ${event.details}`); + } +); +``` -### `onUnsubscribe` + + + +```csharp +Not implemented +``` + + + + +**See also:** + +- [Events](./Events) +- [EventHandler](./Events#eventhandler) +- [PrivateChannelEvent](./Events#privatechannelevent) +- [PrivateChannelAddContextListenerEvent](./Events#privatechanneladdcontextlistenerevent) +- [PrivateChannelUnsubscribeEvent](./Events#privatechannelunsubscribeevent) +- [PrivateChannelDisconnectEvent](./Events#privatechanneldisconnectevent) + +### `disconnect` ```ts -onUnsubscribe(handler: (contextType?: string) => void): Listener; +disconnect(): Promise; ``` ```csharp -IListener OnUnsubscribe(Action handler); +void Disconnect(); ``` -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. +## Deprecated Functions -**See also:** +### `onAddContextListener` -- [`Listener`](Types#listener) + + -### `onDisconnect` +```ts +onAddContextListener(handler: (contextType?: string) => void): Listener; +``` + + + + +```csharp +IListener OnAddContextListener(Action handler); +``` + + + + +Deprecated in favour of the async `addEventListener("addContextListener", handler)` function. + +Adds a listener that will be called each time that the remote app invokes addContextListener on this channel. + +### `onUnsubscribe` ```ts -onDisconnect(handler: () => void): Listener; +onUnsubscribe(handler: (contextType?: string) => void): Listener; ``` - + ```csharp -IListener OnDisconnect(Action handler); +IListener OnUnsubscribe(Action handler); ``` -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("unsubscribe", handler)` function. -- [`disconnect`](#disconnect) +Adds a listener that will be called whenever the remote app invokes `Listener.unsubscribe()` on a context listener that it previously added. -### `disconnect` +### `onDisconnect` ```ts -disconnect(): void; +onDisconnect(handler: () => void): Listener; ``` ```csharp -void Disconnect(); +IListener OnDisconnect(Action handler); ``` -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:** +Deprecated in favour of the async `addEventListener("disconnect", handler)` function. -- [`onUnsubscribe`](#onunsubscribe) -- [`Listener`](Types#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. diff --git a/docs/api/ref/Types.md b/docs/api/ref/Types.md index 16126d5ec..d77f942ab 100644 --- a/docs/api/ref/Types.md +++ b/docs/api/ref/Types.md @@ -254,14 +254,14 @@ Represented as a union type in TypeScript, however, this type may be rendered as ## `Listener` -A Listener object is returned when an application subscribes to intents or context broadcasts via the [`addIntentListener`](DesktopAgent#addintentlistener) or [`addContextListener`](DesktopAgent#addcontextlistener) methods on the [DesktopAgent](DesktopAgent) object. +A Listener object is returned when an application subscribes to intents or context broadcasts via the [`addIntentListener`](DesktopAgent#addintentlistener), [`addContextListener`](DesktopAgent#addcontextlistener) or [`addEventListener`](DesktopAgent#addeventlistener) on the [DesktopAgent](DesktopAgent) object or (PrivateChannel#addeventlistener) on the [PrivateChannel](PrivateChannel) object. ```ts interface Listener { - unsubscribe(): void; + unsubscribe(): Promise; } ``` @@ -284,7 +284,7 @@ interface IListener ```ts -unsubscribe(): void; +unsubscribe(): Promise; ``` diff --git a/docs/api/specs/browserResidentDesktopAgents.md b/docs/api/specs/browserResidentDesktopAgents.md index aa6abe3f6..7955b560d 100644 --- a/docs/api/specs/browserResidentDesktopAgents.md +++ b/docs/api/specs/browserResidentDesktopAgents.md @@ -1,7 +1,7 @@ --- id: browserDesktopAgents sidebar_label: Browser Desktop Agents -title: Browser Desktop Agents (next) +title: Browser-Resident Desktop Agents (next) --- :::info _[@experimental](../fdc3-compliance#experimental-features)_ @@ -206,7 +206,7 @@ Checking whether an application has closed may be achieved by a number of approa - If an equivalent `WindowProxy` object (`WindowProxy` objects can be compared with `==` and will be equivalent if they represent the same window) is received from a different application the DA should consider the original application using that `WindowProxy` to have closed. - By receiving a `WCP6Goodbye` message from the application when it is closing. The `getAgent()` implementation automates the sending of this message via the HTML Standard's [Page Life Cycle API](https://wicg.github.io/page-lifecycle/spec.html). Specifically, the `getAgent()` implementation MUST attempt to detect windows closing by listening for the `pagehide` event and considering a window to be closed if the event's `persisted` property is `false`. - Note that the pagehide event may not fire if the window's render thread crashes or is closed while 'frozen'. -- By polling the application for responses via the `heartbeatEvent` and `heartbeatAcknowledgement` messages provided in the [Desktop Agent Communication Protocol](./desktopAgentCommunicationProtocol). These message may be used for both periodic and on-demand polling by DA implementations. On-demand polling could, for example, be used to check that all instances returned in a findIntent response or displayed in an intent resolver are still alive. +- By polling the application for responses via the `heartbeatEvent` and `heartbeatAcknowledgement` messages provided in the [Desktop Agent Communication Protocol](./desktopAgentCommunicationProtocol#checking-apps-are-alive). These message may be used for both periodic and on-demand polling by DA implementations. On-demand polling could, for example, be used to check that all instances returned in a findIntent response or displayed in an intent resolver are still alive. - Desktop Agents MAY determine their own timeout, or support configuration, to be used for considering an application to have closed as this may be affected by the implementation details of app and DAs. Finally, Desktop Agents SHOULD retain instance details for applications that have closed as they may appear to close during navigation events, or may navigate away and then navigate back. By retaining the instance data (`instanceId`, `instanceUuid` and `WindowProxy`) the same instance identity can be maintained or reissued. There is no standard length of time that such details should be retained, hance, Desktop Agents MAY determine for themselves how long to retain instance details for closed instances. diff --git a/docs/api/specs/desktopAgentCommunicationProtocol.md b/docs/api/specs/desktopAgentCommunicationProtocol.md index 2be68ab96..46f59ce1b 100644 --- a/docs/api/specs/desktopAgentCommunicationProtocol.md +++ b/docs/api/specs/desktopAgentCommunicationProtocol.md @@ -12,75 +12,438 @@ The Desktop Agent Communication Protocol (DACP) is an experimental feature added ::: -DACP constitutes a set of messages that are used by the `@finos/fdc3` library to communicate with Browser-Resident DAs. Each message takes the form of a Flux Standard Action (FSA). Communications are bidirectional and occur over HTML standard MessagePorts. All messages are query/response. Responses may contain requested data or may simply be acknowledgement of receipt. +The Desktop Agent Communication Protocol (DACP) constitutes a set of standardized JSON messages or 'wire protocol' that can be used to implement an interface to a Desktop Agent, encompassing all API calls events defined in the [Desktop Agent API](../ref/DesktopAgent.md). For example, the DACP is used by the [`@finos/fdc3` npm module](https://www.npmjs.com/package/@finos/fdc3) to communicate with Browser-Resident Desktop Agents or a connection setup via the [FDC3 Web Connection Protocol](./webConnectionProtocol). -:::note +## Protocol conventions + +DACP messages are defined in [JSON Schema](https://json-schema.org/) in the [FDC3 github repository](https://github.com/finos/FDC3/tree/fdc3-for-web/schemas/api). + +:::tip + +TypeScript types representing all DACP and WCP messages are generated from the JSON Schema source and can be imported from the [`@finos/fdc3` npm module](https://www.npmjs.com/package/@finos/fdc3): -We refer to "the library" to mean the code imported from `@finos/fdc3` and initiated from a call to `getAgent()`. +```ts +import {BrowserTypes} from '@finos/fdc3'; +``` ::: -Type definitions for all DACP messages can be found here: [bcp.ts](TODO). +The protocol is composed of several different classes of message, each governed by a message schema: -## Protocol conventions +1. **App Request Messages** ([`AppRequest` schema](https://fdc3.finos.org/schemas/next/api/appRequest.schema.json)): + - Messages sent by an application representing an API call, such as [`DesktopAgent.broadcast`](../ref/DesktopAgent#broadcast), [`Channel.addContextListener`](../ref/Channel#addcontextlistener), or [`Listener.unsubscribe`](../ref/Types#listener). + - Message names all end in 'Request'. + - Each instance of a request message sent is uniquely identified by a `meta.requestUuid` field. + +2. **Agent Response Messages** ([`AgentResponse` schema](https://fdc3.finos.org/schemas/next/api/agentResponse.schema.json)): + - Response messages sent from the DA to the application, each relating to a corresponding _App Request Message_. + - Message names all end in 'Response'. + - Each instance of an Agent Response Message is uniquely identified by a `meta.responseUuid` field. + - Each instance of an Agent Response Message quotes the `meta.requestUuid` value of the message it is responding to. + +3. **Agent Event Messages** ([`AgentEvent` schema](https://fdc3.finos.org/schemas/next/api/agentEvent.schema.json)): + - Messages sent from the DA to the application that are due to actions in other applications, such as an inbound context resulting from another app's broadcast. + - Message names all end in 'Event'. + - Each instance of an Agent Response Message is uniquely identified by a `meta.eventUuid` field. + +Each individual message is also governed by a message schema, which is composed with the schema for the message type. + +:::info + +In rare cases, the payload of a request or event message may quote the `requestUuid` or `eventUuid` of another message that it represents a response to, e.g. `intentResultRequest` quotes the `eventUuid` of the `intentEvent` that delivered the intent and context to the app, as well as the `requestUuid` of the `raiseIntentRequest` message that originally raised the intent. + +::: + +All messages defined in the DACP follow a common structure: + +```json +{ + "type": "string", // string identifying the message type + "payload": { + //message payload fields defined for each message type + }, + "meta": { + "timestamp": "2024-09-17T10:15:39+00:00" + //other meta fields determined by each 'class' of message + // these include requestUuid, responseUuid and eventUuid + // and a source field identifying an app where appropriate + } +} +``` + +`meta.timestamp` fields are formatted as strings, according to the format defined by [ISO 8601-1:2019](https://www.iso.org/standard/70907.html), which is produced in JavaScript via the `Date` class's `toISOString()` function, e.g. `(new Date()).toISOString()`. + +### Routing, Registering Listeners & Multiplexing + +The design of the Desktop Agent Communication Protocol is guided by the following sentence from the introduction to the Desktop Agent overview: + +> A Desktop Agent is a desktop component (or aggregate of components) that serves as a launcher and message router (broker) for applications in its domain. + +Hence, that design is based on the assumption that all messaging between applications passes through an entity that acts as the 'Desktop Agent' and routes those messages on to the appropriate recipients (for example a context message broadcast by an app to a channel is routed onto other apps that have added a listener to that channel, or an intent and context pair raised by an application is routed to another app chosen to resolve that intent). While implementations based on a shared bus are possible, they have not been specifically considered in the design of the DACP messages. + +Further, the design of the DACP is based on the assumption that applications will interact with an implementation of the [`DesktopAgent`](../ref/DesktopAgent) interface, with the DACP used behind the scenes to support communication between the implementation of that interface and an entity acting as the Desktop Agent which is running in another process or location, necessitating the use of a 'wire protocol' for communication. For example, [Browser-Resident Desktop Agent](./browserResidentDesktopAgents) implementations use the [FDC3 Web Communication Protocol (WCP)](./webConnectionProtocol.md) to connect a 'Desktop Agent Proxy', provided by the `getAgent()` implementation in the [`@finos/fdc3` npm module], and a Desktop Agent running in another frame or window which is communicated with via the DACP. + +As a Desktop Agent is expected to act as a router for messages sent through the Desktop Agent API, the DACP provides message exchanges for the registration and unregistration of listeners for particular message types (e.g. events, contexts broadcast on user channels, contexts broadcast on other channel types, raised intents etc.). In most cases, apps can register multiple listeners for the same messages (often filtered for different context or event types). However, where multiple listeners are present, only a single DACP message should be sent representing the action taken in the FDC3 API (e.g. broadcasting a message to a channel) and any multiplexing to multiple listeners should be applied at the receiving end. For example, when working with the WCP, this should be handled by the Desktop Agent Proxy implementation provided by the `getAgent()` implementation. + +## Message Definitions Supporting FDC3 API calls + +This section provides details of the messages defined in the DACP, grouped according to the FDC3 API functions that they support, and defined by JSON Schema files. Many of these message definitions make use of JSON versions of [metadata](../ref/Metadata) and other [types](../ref/Types) defined by the Desktop Agent API, the JSON versions of which can be found in [api.schema.json](https://fdc3.finos.org/schemas/next/api/api.schema.json), while a number of DACP specific object definitions that are reused through the messages can be found in [common.schema.json](https://fdc3.finos.org/schemas/next/api/common.schema.json). + +### `DesktopAgent` + +#### `addContextListener()` + +Request and response used to implement the [`DesktopAgent.addContextListener()`](../ref/DesktopAgent#addcontextlistener) and [`Channel.addContextListener()`](../ref/Channel#addcontextlistener) API calls: + +- [`addContextListenerRequest`](https://fdc3.finos.org/schemas/next/api/addContextListenerRequest.schema.json) +- [`addContextListenerResponse`](https://fdc3.finos.org/schemas/next/api/addContextListenerResponse.schema.json) + +Event message used to deliver context objects that have been broadcast to listeners: + +- [`broadcastEvent`](https://fdc3.finos.org/schemas/next/api/broadcastEvent.schema.json) + +Request and response for removing the context listener ([`Listener.unsubscribe()`](../ref/Types#listener)): + +- [`contextListenerUnsubscribeRequest`](https://fdc3.finos.org/schemas/next/api/contextListenerUnsubscribeRequest.schema.json) +- [`contextListenerUnsubscribeResponse`](https://fdc3.finos.org/schemas/next/api/contextListenerUnsubscribeResponse.schema.json) + +#### `addEventListener()` + +Request and response used to implement the [`addEventListener()`](../ref/DesktopAgent#addeventlistener) API call: + +- [`addEventListenerRequest`](https://fdc3.finos.org/schemas/next/api/addEventListenerRequest.schema.json) +- [`addEventListenerResponse`](https://fdc3.finos.org/schemas/next/api/addEventListenerResponse.schema.json) + +Event messages used to deliver events that have occurred: + +- [`channelChangedEvent`](https://fdc3.finos.org/schemas/next/api/channelChangedEvent.schema.json) + +Request and response for removing the event listener ([`Listener.unsubscribe()`](../ref/Types#listener)): + +- [`eventListenerUnsubscribeRequest`](https://fdc3.finos.org/schemas/next/api/eventListenerUnsubscribeRequest.schema.json) +- [`eventListenerUnsubscribeResponse`](https://fdc3.finos.org/schemas/next/api/eventListenerUnsubscribeResponse.schema.json) + +#### `addIntentListener()` + +Request and response used to implement the [`addIntentListener()`](../ref/DesktopAgent#addintentlistener) API call: + +- [`addIntentListenerRequest`](https://fdc3.finos.org/schemas/next/api/addIntentListenerRequest.schema.json) +- [`addIntentListenerResponse`](https://fdc3.finos.org/schemas/next/api/addIntentListenerResponse.schema.json) + +Event message used to a raised intent and context object from another app to the listener: + +- [`intentEvent`](https://fdc3.finos.org/schemas/next/api/intentEvent.schema.json) + +An additional request and response used to deliver an [`IntentResult`](../ref/Types#intentresult) from the intent handler to the Desktop Agent, so that it can convey it back to the raising application: + +- [`intentResultRequest`](https://fdc3.finos.org/schemas/next/api/intentResultRequest.schema.json) +- [`intentResultResponse`](https://fdc3.finos.org/schemas/next/api/intentResultResponse.schema.json) + +Please note this exchange (and the `IntentResolution.getResult()` API call) support `void` results from a raised intent and hence this message exchange should occur for all raised intents, including those that do not return a result. In such cases, the void intent result allows resolution of the `IntentResolution.getResult()` API call and indicates that the intent handler has finished running. + +Request and response for removing the intent listener ([`Listener.unsubscribe()`](../ref/Types#listener)):: + +- [`intentListenerUnsubscribeRequest`](https://fdc3.finos.org/schemas/next/api/intentListenerUnsubscribeRequest.schema.json) +- [`intentListenerUnsubscribeResponse`](https://fdc3.finos.org/schemas/next/api/intentListenerUnsubscribeResponse.schema.json) + +A typical exchange of messages between an app raising an intent, a Desktop agent and an app resolving an intent is: + +```mermaid +sequenceDiagram + AppA ->> DesktopAgent: raiseIntentRequest + DesktopAgent ->> AppB: intentEvent + DesktopAgent ->> AppA: raiseIntentResponse + AppB ->> DesktopAgent: intentResultRequest + DesktopAgent ->> AppB: intentResultResponse + DesktopAgent ->> AppA: raiseIntentResultResponse +``` + +The above flow assumes that AppB has already been launched and added an intent listener. As apps can be launched to resolve an intent a typical message exchange (that includes registration of the intent listener) is: + +```mermaid +sequenceDiagram + AppA ->> DesktopAgent: raiseIntentRequest + break intent resolution determines a new instance of AppB should be launched + DesktopAgent -->> AppB: Launch + AppB -->> DesktopAgent: Connect via WCP + end + AppB ->> DesktopAgent: addIntentListenerRequest + DesktopAgent ->> AppB: addIntentListenerResponse + DesktopAgent ->> AppB: intentEvent + DesktopAgent ->> AppA: raiseIntentResponse + AppB ->> DesktopAgent: intentResultRequest + DesktopAgent ->> AppB: intentResultResponse + DesktopAgent ->> AppA: raiseIntentResultResponse +``` + +:::tip + +See [`raiseIntent`](#raiseintent) below for further examples of message exchanges involved in raising intents and intent resolution. + +::: + +#### `broadcast()` + +Request and response used to implement the [`DesktopAgent.broadcast()`](../ref/DesktopAgent#broadcast) and [`Channel.broadcast()`](../ref/Channel#broadcast) API calls: + +- [`broadcastRequest`](https://fdc3.finos.org/schemas/next/api/broadcastRequest.schema.json) +- [`broadcastResponse`](https://fdc3.finos.org/schemas/next/api/broadcastResponse.schema.json) + +See [`addContextListener()`](#addcontextlistener) above for the `broadcastEvent` used to deliver the broadcast to other apps. + +#### `createPrivateChannel()` + +Request and response used to implement the [`createPrivateChannel()`](../ref/DesktopAgent#createprivatechannel) API call: + +- [`createPrivateChannelRequest`](https://fdc3.finos.org/schemas/next/api/createPrivateChannelRequest.schema.json) +- [`createPrivateChannelResponse`](https://fdc3.finos.org/schemas/next/api/createPrivateChannelResponse.schema.json) -The protocol is divided into groups of messages: +#### `findInstances()` -1) Messages sent from the library to the DA. These typically have a 1:1 correspondence with function calls on `DesktopAgent` and `Channel`, and ancillary functionality such as unsubscribing. +Request and response used to implement the [`findInstances()`](../ref/DesktopAgent#findinstances) API call: -2) Response messages, sent from the DA to the library. Every message sent from the library to the DA will receive a response. In most cases, the type will simply have "Response" appended. For instance, the response message for `getInfo` is `getInfoResponse`. For all other cases the `DACPAck` message will be the response. Every response's payload will contain an error string if an error occurred, otherwise it will contain the expected data. +- [`findInstancesRequest`](https://fdc3.finos.org/schemas/next/api/findInstancesRequest.schema.json) +- [`findInstancesResponse`](https://fdc3.finos.org/schemas/next/api/findInstancesResponse.schema.json) -3) Asynchronous "inbound" messages, sent from the DA to the library. These messages are due to actions in other apps, such as an inbound context resulting from another app's broadcast. These messages have the name of the originating message appended with `Inbound`. For example, if another app called `broadcast` then this app would receive a message called `broadcastInbound`. +#### `findIntent()` -Every message has a `meta.messageId`. Initiating messages must set this to be a unique string. Response messages must use the string from their corresponding initiating message. The `messageId` can be used by library and DA to match incoming message responses with their initial requests. +Request and response used to implement the [`findIntent()`](../ref/DesktopAgent#findintent) API call: -## Multiplexing +- [`findIntentRequest`](https://fdc3.finos.org/schemas/next/api/findIntentRequest.schema.json) +- [`findIntentResponse`](https://fdc3.finos.org/schemas/next/api/findIntentResponse.schema.json) -For any given contextType or intent, the library should only ever send `DACPAddContextListener` or `DACPAddIntentListener` one time. The DA is only responsible for sending any given Context or Intent _once_ to an app. The DA may ignore duplicate listener registrations. +#### `findIntentsByContext()` -If the app has registered multiple listeners for these types then it is the responsibility of the _library_ to multiplex the delivered Context, or to choose a specific intent listener. +Request and response used to implement the [`findIntentsByContext()`](../ref/DesktopAgent#findintentsbycontext) API call: -When the API calls the unsubscriber for a listener then `DACPRemoveContextListener` or `DACPRemoveIntentListener` should be sent to the DA. +- [`findIntentsByContextRequest`](https://fdc3.finos.org/schemas/next/api/findIntentsByContextRequest.schema.json) +- [`findIntentsByContextResponse`](https://fdc3.finos.org/schemas/next/api/findIntentsByContextResponse.schema.json) -## Intents +#### `getAppMetadata()` -Refer [Private Channel examples](../ref/PrivateChannel.md#server-side-example) to understand how intent transactions work. +Request and response used to implement the [`getAppMetadata()`](../ref/DesktopAgent#getappmetadata) API call: -When an app ("client") calls `raiseIntent()` or `raiseIntentByContext()`, the library MUST send the corresponding `DACPRaiseIntent` or `DACPRaiseIntentByContext` message to the DA. +- [`getAppMetadataRequest`](https://fdc3.finos.org/schemas/next/api/getAppMetadataRequest.schema.json) +- [`getAppMetadataResponse`](https://fdc3.finos.org/schemas/next/api/getAppMetadataResponse.schema.json) -The DA will resolve the intent and then deliver a `DACPIntentInbound` message to the library in the resolved app ("server"). This message will contain a `responseId` that has been generated by the DA. +#### `getCurrentChannel()` -After the message has been sent, the DA will respond back to the "client" app's library with a `DACPRaiseIntentResponse` message containing that `responseId`. +Request and response used to implement the [`getCurrentChannel()`](../ref/DesktopAgent#getcurrentchannel) API call: -If the "client" app then calls `getResult()`, then the library will wait until a `DACPIntentResult` message is received with a corresponding `responseId`. It will resolve the `getResult()` call with either a Context or PrivateChannel depending on the contents of the result. +- [`getCurrentChannelRequest`](https://fdc3.finos.org/schemas/next/api/getCurrentChannelRequest.schema.json) +- [`getCurrentChannelResponse`](https://fdc3.finos.org/schemas/next/api/getCurrentChannelResponse.schema.json) -Meanwhile, if the "server" app's intent handler resolves to a Channel or Context then the library should send a `DACPIntentResult` with the `responseId` that was initially received from `DACPIntentInbound`. +#### `getInfo()` +Request and response used to implement the [`getInfo()`](../ref/DesktopAgent#getinfo) API call: -## Intent Resolver +- [`getInfoRequest`](https://fdc3.finos.org/schemas/next/api/getInfoRequest.schema.json) +- [`getInfoResponse`](https://fdc3.finos.org/schemas/next/api/getInfoResponse.schema.json) -The DA should send `DACPResolveIntent` if it requires an external UI for intent resolution. This MUST include the list of available apps which are capable of being launched to handle the intent, and it MUST include the list of open apps which are capable of handling the intent. The "@finos/fdc3" library will present UI to the end user, and then will respond with a `DACPResolveIntentResponse` containing the user's choice. +#### `getOrCreateChannel()` -DAs are free to provide their own intent resolution UIs if they have this capability. +Request and response used to implement the [`getOrCreateChannel()`](../ref/DesktopAgent#getorcreatechannel] API call: -## Channels +- [`getOrCreateChannelRequest`](https://fdc3.finos.org/schemas/next/api/getOrCreateChannelRequest.schema.json) +- [`getOrCreateChannelResponse`](https://fdc3.finos.org/schemas/next/api/getOrCreateChannelResponse.schema.json) -The DA should send `DACPInitializeChannelSelector` if it requires the app to provide UI for channel selection. The "@finos/fdc3" library will provide the UI when this message is received. +#### `getUserChannels()` -Any message related to a channel contains a `channelId` field. It is the responsibility of each party (DA and library) to correlate `channelId` fields with the correct local objects. +Request and response used to implement the [`getUserChannels()`](../ref/DesktopAgent#getuserchannels) API call: + +- [`getUserChannelsRequest`](https://fdc3.finos.org/schemas/next/api/getUserChannelsRequest.schema.json) +- [`getUserChannelsResponse`](https://fdc3.finos.org/schemas/next/api/getUserChannelsResponse.schema.json) + +#### `joinUserChannel()` + +Request and response used to implement the [`joinUserChannel()`](../ref/DesktopAgent#joinchannel) API call: + +- [`joinUserChannelRequest`](https://fdc3.finos.org/schemas/next/api/joinUserChannelRequest.schema.json) +- [`joinUserChannelResponse`](https://fdc3.finos.org/schemas/next/api/joinUserChannelResponse.schema.json) + +#### `leaveCurrentChannel()` + +Request and response used to implement the [`leaveCurrentChannel()`](../ref/DesktopAgent#leavecurrentchannel) API call: + +- [`leaveCurrentChannelRequest`](https://fdc3.finos.org/schemas/next/api/leaveCurrentChannelRequest.schema.json) +- [`leaveCurrentChannelResponse`](https://fdc3.finos.org/schemas/next/api/leaveCurrentChannelResponse.schema.json) + +#### `open()` + +Request and response used to implement the [`open()`](../ref/DesktopAgent#open) API call: + +- [`openRequest`](https://fdc3.finos.org/schemas/next/api/openRequest.schema.json) +- [`openResponse`](https://fdc3.finos.org/schemas/next/api/openResponse.schema.json) + +Where a context object is passed (e.g. `fdc3.open(app, context)`) the `broadcastEvent` message described above in [`addContextListener`](#addcontextlistener) should be used to deliver it after the context listener has been added: + +```mermaid +sequenceDiagram + AppA ->> DesktopAgent: openRequest
(with context) + break Desktop Agent launches AppB + DesktopAgent -->> AppB: Launch + AppB -->> DesktopAgent: Connect via WCP + end + AppB ->> DesktopAgent: addContextListenerRequest + DesktopAgent ->> AppB: addContextListenerResponse + DesktopAgent ->> AppB: broadcastEvent +``` + +#### `raiseIntent()` + +Request and response used to implement the [`raiseIntent()`](../ref/DesktopAgent#raiseintent) API call: + +- [`raiseIntentRequest`](https://fdc3.finos.org/schemas/next/api/raiseIntentRequest.schema.json) +- [`raiseIntentResponse`](https://fdc3.finos.org/schemas/next/api/raiseIntentResponse.schema.json) + +An additional response message is provided for the delivery of an `IntentResult` from the resolving application to the raising application (which is collected via the [`IntentResolution.getResult()`](../ref/Metadata#intentresolution) API call), which should quote the `requestUuid` from the original `raiseIntentRequest`: + +- [`raiseIntentResultResponse`](https://fdc3.finos.org/schemas/next/api/raiseIntentResultResponse.schema.json) + +:::tip + +See [`addIntentListener`](#addintentlistener) above for details of the messages used for the resolving app to deliver the result to the Desktop Agent. + +::: + +Where there are multiple options for resolving a raised intents, there are two possible versions of the resulting message exchanges. Which to use depends on whether the Desktop Agent uses an intent resolver user interface (or other suitable mechanism) that it controls, or one injected into the application (for example an iframe injected by a `getAgent()` implementation into an application window) to perform resolution. + +When working with an injected interface, the Desktop Agent should respond with a `raiseIntentResponse` containing a `RaiseIntentNeedsResolutionResponsePayload`: + +```mermaid +--- +title: Intent resolution with injected Intent Resolver iframe +--- +sequenceDiagram + AppA ->> DesktopAgent: raiseIntentRequest + DesktopAgent ->> AppB: intentEvent + DesktopAgent ->> AppA: raiseIntentResponse + Note left of DesktopAgent: raiseIntentResponse includes a
RaiseIntentNeedsResolutionResponsePayload
containing an AppIntent + break when AppIntent return with multiple options + DesktopAgent --> AppA: getAgent displays IntentResolver + AppA --> DesktopAgent: User picks an option + end + AppA ->> DesktopAgent: raiseIntentRequest + Note left of DesktopAgent: New request includes a
specific 'app' target
and new requestUuid + DesktopAgent ->> AppB: intentEvent + DesktopAgent ->> AppA: raiseIntentResponse + AppB ->> DesktopAgent: intentResultRequest + DesktopAgent ->> AppB: intentResultResponse + DesktopAgent ->> AppA: raiseIntentResultResponse +``` + +Alternatively, if the Desktop Agent is able to provide its own user interface or another suitable means of resolving the intent, then it may do so and respond with a `raiseIntentResponse` containing a `RaiseIntentSuccessResponsePayload`: + +```mermaid +--- +title: Intent resolution with Desktop Agent provided Intent Resolver +--- +sequenceDiagram + AppA ->> DesktopAgent: raiseIntentRequest + DesktopAgent ->> AppB: intentEvent + break DA determines there are multiple options + DesktopAgent-->AppA: Desktop Agent displays an
IntentResolver UI + AppA-->DesktopAgent: User picks an option + end + DesktopAgent ->> AppA: raiseIntentResponse + Note left of DesktopAgent: DesktopAgent responds
to the original
raiseIntentRequest message with
a RaiseIntentSuccessResponsePayload + AppB ->> DesktopAgent: intentResultRequest + DesktopAgent ->> AppB: intentResultResponse + DesktopAgent ->> AppA: raiseIntentResultResponse +``` + +#### `raiseIntentForContext()` + +Request and response used to implement the [`raiseIntentForContext()`](../ref/DesktopAgent#raiseintentforcontext) API call: + +- [`raiseIntentForContextRequest`](https://fdc3.finos.org/schemas/next/api/raiseIntentForContextRequest.schema.json) +- [`raiseIntentForContextResponse`](https://fdc3.finos.org/schemas/next/api/raiseIntentForContextResponse.schema.json) + +Message exchanges for handling `raiseIntentForContext()` are the same as for `raiseIntent`, except for the substitution of `raiseIntentForContextRequest` for `raiseIntentRequest` and `raiseIntentForContextResponse` for `raiseIntentResponse`. Hence, please see [`raiseIntent`](#raiseintent) and [`addIntentListener`](#addintentlistener) for further details. + +### `Channel` + +Owing to the significant overlap between the FDC3 [`DesktopAgent`](../ref/DesktopAgent) and [`Channel`](../ref/Channel) interfaces, which includes the ability to retrieve and work with User channels as App Channels, most of the messaging for the `Channel` API is shared with `DesktopAgent`. Specifically, all messages defined in the the [`broadcast`](#broadcast) and [`addContextListener`](#addcontextlistener) sections above are reused, with a few minor differences to note: + +- When working with a specific channel, the `channelId` property in `addContextListenerRequest` should be set to the ID of the channel, where it is set to `null` to work with the current user channel. +- When receiving a `broadcastEvent` a `channelId` that is `null` indicates that the context was sent via a call to `fdc3.open` and does not relate to a channel. + +The following additional function is unique to the `Channel` interface: + +#### `getCurrentContext()` + +Request and response used to implement the [`Channel.getCurrentContext()`](../ref/Channel#getcurrentcontext) API call: + +- [`getCurrentContextRequest`](https://fdc3.finos.org/schemas/next/api/getCurrentContextRequest.schema.json) +- [`getCurrentContextResponse`](https://fdc3.finos.org/schemas/next/api/getCurrentContextResponse.schema.json) + +### `PrivateChannel` + +The [`PrivateChannel`](../ref/PrivateChannel) interface extends [`Channel`](../ref/Channel) with a number of additional functions that are supported by the following messages: + +#### `addEventListener()` + +Request and response used to implement the [`PrivateChannel.addEventListener`](../ref/PrivateChannel#addeventlistener) API call: + +- [`privateChanneladdEventListenerRequest`](https://fdc3.finos.org/schemas/next/api/privateChanneladdEventListenerRequest.schema.json) +- [`privateChanneladdEventListenerResponse`](https://fdc3.finos.org/schemas/next/api/privateChanneladdEventListenerResponse.schema.json) + +Event messages used to deliver events that have occurred: + +- [`privateChannelOnAddContextListenerEvent`](https://fdc3.finos.org/schemas/next/api/privateChannelOnAddContextListenerEvent.schema.json) +- [`privateChannelOnDisconnectEvent`](https://fdc3.finos.org/schemas/next/api/privateChannelOnDisconnectEvent.schema.json) +- [`privateChannelOnUnsubscribeEvent`](https://fdc3.finos.org/schemas/next/api/privateChannelOnUnsubscribeEvent.schema.json) + +:::tip + +The above messages may also be used to implement the deprecated [`onAddContextListener()`](../ref/PrivateChannel#onaddcontextlistener), [`onUnsubscribe`](../ref/PrivateChannel#onunsubscribe) and [`onDisconnect`](../ref/PrivateChannel#ondisconnect) functions of the `PrivateChannel` interface. + +::: + +Message exchange for removing the event listener [`Listener.unsubscribe`](../ref/Types#listener): + +- [`privateChannelUnsubscribeEventListenerRequest`](https://fdc3.finos.org/schemas/next/api/privateChannelUnsubscribeEventListenerRequest.schema.json) +- [`privateChannelUnsubscribeEventListenerResponse`](https://fdc3.finos.org/schemas/next/api/privateChannelUnsubscribeEventListenerResponse.schema.json) + +#### `disconnect()` + +Request and response used to implement the [`PrivateChannel.disconnect()`](../ref/PrivateChannel#disconnect) API call: + +- [`privateChannelDisconnectRequest`](https://fdc3.finos.org/schemas/next/api/privateChannelDisconnectRequest.schema.json) +- [`privateChannelDisconnectResponse`](https://fdc3.finos.org/schemas/next/api/privateChannelDisconnectResponse.schema.json) + +### Checking apps are alive + +Depending on the connection over which the Desktop Agent and app are connected, it may be necessary for the Desktop Agent to check whether the application is still alive. This can be done, either periodically or on demand (for example to validate options that will be provided in an [`AppIntent`](../ref/Metadata#appintent) as part of a `findIntentResponse` or `raiseIntentResponse` and displayed in an intent resolver interface), using the following message exchange: + +- [`heartbeatEvent`](https://fdc3.finos.org/schemas/next/api/heartbeatEvent.schema.json) +- [`heartbeatAcknowledgment`](https://fdc3.finos.org/schemas/next/api/heartbeatAcknowledgment.schema.json) + +As a Desktop Agent initiated exchange, it is initiated with an `AgentEvent` message and completed via an `AppRequest` message as an acknowledgement. + +:::tip + +Additional procedures are defined in the [Browser Resident Desktop Agents specification](./browserResidentDesktopAgents#disconnects) and [Web Connection Protocol](./webConnectionProtocol#step-5-disconnection) for the detection of app disconnection or closure. Implementations will often need to make use of multiple procedures to catch all forms of disconnection in a web browser. + +::: -For instance, when the library receives a `DACPBroadcastInbound` message, it should look for the `channelId` field, and only deliver that message to listeners on the corresponding local `Channel` object. +### Controlling injected User Interfaces -Likewise, when an app calls `channel.broadcast()` then the library should send the `DACPBroadcast` message with the `channelId` set accordingly. +Desktop Agent implementations, such as those based on the [Browser Resident Desktop Agents specification](./browserResidentDesktopAgents) and [Web Connection Protocol](./webConnectionProtocol), may either provide their own user interfaces (or other appropriate mechanisms) for the selection of User Channels or Intent Resolution, or they may work with implementations injected into the application (for example, as described in the [Web Connection Protocol](./webConnectionProtocol#providing-channel-selector-and-intent-resolver-uis) and implemented in [`getAgent()`](../ref/GetAgent)). -## Private Channels +Where injected user interfaces are used, standardized messaging is needed to communicate with those interfaces. This is provided in the DACP via the following 'iframe' messages, which are governed by the [`iFrameMessage`](https://fdc3.finos.org/schemas/next/api/iFrameMessage.schema.json) schema. The following messages are provided: -In general, private channels behave as channels. The DA MUST assign a unique `channelId` in response to `DACPCreatePrivateChannel` messages. `DACPBroadcast` and `DACPAddContextListener` messages can be transmitted with this `channelId`. +- [`iFrameHello`](https://fdc3.finos.org/schemas/next/api/iFrameHello.schema.json): Sent by the iframe to its `window.parent` frame to initiate communication and to provide initial CSS to apply to the frame. This message should have a `MessagePort` appended over which further communication will be conducted. +- [`iFrameHandshake`](https://fdc3.finos.org/schemas/next/api/iFrameHandshake.schema.json): Response to the `iFrameHello` message sent by the application frame, which should be sent over the `MessagePort`. Includes details of the FDC3 version that the application is using. +- [`iFrameDrag`](https://fdc3.finos.org/schemas/next/api/iFrameDrag.schema.json): Message sent by the iframe to indicate that it is being dragged to a new position and including offsets to indicate direction and distance. +- [`iFrameRestyle`](https://fdc3.finos.org/schemas/next/api/iFrameRestyle.schema.json): Message sent by the iframe to indicate that its frame should have updated CSS applied to it, for example to support a channel selector interface that can be 'popped open' or an intent resolver that wishes to resize itself to show additional content. -See [Intents](#intents) for the process that is used to established a private channel. +Messages are also provided that are specific to each interface type provided by a Desktop Agent. The following messages are specific to Channel Selector user interfaces: -FDC3's `PrivateChannel` object has some specific functions, each of which has a corresponding DACP message. For instance, `PrivateChannel.onAddContextListener()` can be implemented using the `DACPPrivateChannelOnAddContextListener` message. Each of these types of messages contains a `channelId` which can be used to identify the channel. +- [`iFrameChannels`](https://fdc3.finos.org/schemas/next/api/iFrameChannels.schema.json): Sent by the parent frame to initialize a Channel Selector user interface by providing metadata for the Desktop Agent's user channels and details of any channel that is already selected. This message will typically be sent by a `getAgent()` implementation immediately after the `iFrameHandshake` and before making the injected iframe visible. +- [`iFrameChannelSelected`](https://fdc3.finos.org/schemas/next/api/iFrameChannelSelected.schema.json): Sent by the Channel Selector to indicate that a channel has been selected or deselected. -The DA should send `DACPPrivateChannelOnAddContextListener` and `DACPPrivateChannelOnUnsubscribe` messages whenever `DACPAddContextListener` or `DACPRemoveContextListener` is called on a private channel. These will be delivered to the library regardless of whether a client has actually called `onAddContextListener()` and `onUnsubscribe()`. It is the library's responsibility to track these calls and either deliver or discard the messages accordingly. +Messages specific to Intent Resolver user interfaces: -Likewise, the DA should send `DACPPrivateChannelOnDisconnect` whenever the `DACPPrivateChannelDisconnect` message is received. It is the library's responsibility to deliver or discard this message. +- [`iFrameResolve`](https://fdc3.finos.org/schemas/next/api/iFrameResolve.schema.json): Sent by the parent frame to initialize a Intent Resolver user interface to resolve a raised intent, before making it visible. The message includes the context object sent with the intent and an array of one or more [`AppIntent`](../ref/Metadata#appintent) objects representing the resolution options for the intent ([`raiseIntent`](../ref/DesktopAgent#raiseintent)) or context ([`raiseIntentForContext`](../ref/DesktopAgent#raiseintentforcontext)) that was raised. +- [`iFrameResolveAction`](https://fdc3.finos.org/schemas/next/api/iFrameResolveAction.schema.json): Sent by the Intent Resolver to indicate actions taken by the user in the interface, including hovering over an option, clicking a cancel button, or selecting a resolution option. The Intent Resolver should be hidden by the `getAgent()` implementaiton after a resolution option is selected. diff --git a/docs/api/specs/webConnectionProtocol.md b/docs/api/specs/webConnectionProtocol.md index f32c7f36b..ab1eef269 100644 --- a/docs/api/specs/webConnectionProtocol.md +++ b/docs/api/specs/webConnectionProtocol.md @@ -10,11 +10,11 @@ The FDC3 Web Connection Protocol (WCP) is an experimental feature added to FDC3 ::: -The FDC3 Web Connection Protocol (WCP) defines the procedure for a web-application to connect to an FDC3 Desktop Agent. The WCP is used to implement a [`getAgent()`](../ref/GetAgent) function in the [@finos/fdc3 npm module](https://www.npmjs.com/package/@finos/fdc3), which is the recommended way for web applications to connect to a Desktop Agent. This specification details how it retrieves and provides the FDC3 `DesktopAgent` interface object and requirements that Desktop Agents must implement in order to support discovery and connection via `getAgent()`. Please see the [`getAgent` reference document](../ref/GetAgent.md) for its TypeScript definition and related types. +The FDC3 Web Connection Protocol (WCP) defines the procedure for a web-application to connect to an FDC3 Desktop Agent. The WCP is used to implement a [`getAgent()`](../ref/GetAgent) function in the [`@finos/fdc3` npm module](https://www.npmjs.com/package/@finos/fdc3), which is the recommended way for web applications to connect to a Desktop Agent. This specification details how it retrieves and provides the FDC3 `DesktopAgent` interface object and requirements that Desktop Agents must implement in order to support discovery and connection via `getAgent()`. Please see the [`getAgent` reference document](../ref/GetAgent.md) for its TypeScript definition and related types. :::tip -The [@finos/fdc3 npm module](https://www.npmjs.com/package/@finos/fdc3) provides a `getAgent()` implementation which app can use to connect to a Desktop Agent without having to interact with or understand the WCP directly. See [Support Platforms](../supported-platforms) and the [`getAgent()`](../ref/GetAgent) reference page for more details on using `getAgent()` in an application. +The [`@finos/fdc3` npm module](https://www.npmjs.com/package/@finos/fdc3) provides a `getAgent()` implementation which app can use to connect to a Desktop Agent without having to interact with or understand the WCP directly. See [Support Platforms](../supported-platforms) and the [`getAgent()`](../ref/GetAgent) reference page for more details on using `getAgent()` in an application. ::: @@ -37,9 +37,40 @@ Further details for implementing Preload Desktop Agents (which use a Desktop Age ::: -## WCP Message Schemas and Types +## WCP Message Schemas -There are a number of message formats defined as part of the Web Connection Protocol, which will be referenced later in this document, these are: +There are a number of messages defined as part of the Web Connection Protocol. Definitions are provided in [JSON Schema](https://json-schema.org/) in the [FDC3 github repository](https://github.com/finos/FDC3/tree/fdc3-for-web/schemas/api). + +:::tip + +TypeScript types representing all DACP and WCP messages are generated from the JSON Schema source and can be imported from the [`@finos/fdc3` npm module](https://www.npmjs.com/package/@finos/fdc3): + +```ts +import {BrowserTypes} from '@finos.fdc3'; +``` + +::: + +WCP messages are derived from a base schema, [WCPConnectionStep](https://fdc3.finos.org/schemas/next/api/.schema.json), which defines a common structure for the messages: + +```json +{ + "type": "string", // string identifying the message type + "payload": { + //message payload fields defined for each message type + }, + "meta": { + "connectionAttemptUuid": "79be3ff9-7c05-4371-842a-cf08427c174d", + "timestamp": "2024-09-17T10:15:39+00:00" + } +} +``` + +A value for `meta.connectionAttemptUuid` should be generated as a version 4 UUID according to [IETF RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) at the start for the connection process and quoted in all subsequent messages, as described later in this document. + +`meta.timestamp` fields are formatted as strings, according to the format defined by [ISO 8601-1:2019](https://www.iso.org/standard/70907.html), which is produced in JavaScript via the `Date` class's `toISOString()` function, e.g. `(new Date()).toISOString()`. + +Messages defined as part of the Web Connection Protocol, which will be referenced later in this document, these are: - [`WCP1Hello`](https://fdc3.finos.org/schemas/next/api/WCP1Hello.schema.json) - [`WCP2LoadUrl`](https://fdc3.finos.org/schemas/next/api/WCP2LoadUrl.schema.json) @@ -161,9 +192,9 @@ Setup a timer for specified timeout, and then for each `candidate` found, attemp ``` Note that the `targetOrigin` is set to `*` as the origin of the Desktop Agent is not known at this point. - 3. Accept the first correct response received from a candidate. Correct responses MUST correspond to either the [`WCP2LoadUrl`](https://fdc3.finos.org/schemas/next/api/WCP2LoadUrl.schema.json) or [`WCP3Handshake`](https://fdc3.finos.org/schemas/next/api/WCP3Handshake.schema.json) message schemas and MUST quote the same `meta.connectionAttemptUuid` value provided in the original `WCP1Hello` message. Stop the timeout when a correct response is received. + 3. Accept the first correct response received from a candidate. Correct responses MUST correspond to either the [`WCP2LoadUrl`](https://fdc3.finos.org/schemas/next/api/WCP2LoadUrl.schema.json) or [`WCP3Handshake`](https://fdc3.finos.org/schemas/next/api/WCP3Handshake.schema.json) message schemas and MUST quote the same `meta.connectionAttemptUuid` value provided in the original `WCP1Hello` message. Stop the timeout when a correct response is received. If no response is received from any candidate, the `getAgent()` implementation MAY retry sending the `WCP1Hello` message periodically until the timeout is reached. 4. If a [`WCP3Handshake`](https://fdc3.finos.org/schemas/next/api/WCP3Handshake.schema.json) was received in the previous step, skip this this step and move on to 5. However, If a [`WCP2LoadUrl`](https://fdc3.finos.org/schemas/next/api/WCP2LoadUrl.schema.json) was received in the previous step: - * Create a hidden iframe within the page, set its URL to the URL provided by the `payload.iframeUrl` field of the message and add a handler to run when the iframe has loaded: + - Create a hidden iframe within the page, set its URL to the URL provided by the `payload.iframeUrl` field of the message and add a handler to run when the iframe has loaded: ```ts const loadIframe = (url, loadedHandler): WindowProxy => { const ifrm = document.createElement("iframe"); @@ -177,7 +208,14 @@ Setup a timer for specified timeout, and then for each `candidate` found, attemp return ifrm.contentWindow; } ``` - * Once the frame has loaded (i.e. when the `loadedHandler` in the above example runs), repeat steps 1-3 above substituting the the iframe's `contentWindow` for the candidate window objects before proceeding to step 5. A new timeout should be used to limit the amount of time that the `getAgent()` implementation waits for a response. If the event that this subsequent timeout is exceeded, reject Error with the `ErrorOnConnect` message from the [`AgentError`](../ref/Errors#agenterror) enumeration. + - Once the frame has loaded (i.e. when the `loadedHandler` in the above example runs), repeat steps 1-3 above substituting the iframe's `contentWindow` for the candidate window objects before proceeding to step 5. A new timeout should be used to limit the amount of time that the `getAgent()` implementation waits for a response. If the event that this subsequent timeout is exceeded, reject Error with the `ErrorOnConnect` message from the [`AgentError`](../ref/Errors#agenterror) enumeration. + + :::tip + + To ensure that the iframe is ready to receive the `WCP1Hello` message when the `load` event fires, implementations should call `window.addEventListener` for the `message` event synchronously and as early as possible. + + ::: + 5. At this stage, a [`WCP3Handshake`](https://fdc3.finos.org/schemas/next/api/WCP3Handshake.schema.json) message should have be received from either a candidate parent or a hidden iframe created in 4 above. This message MUST have a `MessagePort` appended to it, which is used for further communication with the Desktop Agent. Add a listener (`port.addEventListener("message", (event) => {})`) to receive messages from the selected `candidate`, before moving on to the next stage. @@ -189,6 +227,12 @@ Setup a timer for specified timeout, and then for each `candidate` found, attemp If the failover function resolves to a `WindowProxy` object, repeat steps 1-3 & 5 above substituting the `WindowProxy` for the candidate window objects before proceeding to the next step. + :::tip + + Where possible, iframe failover functions should wait for the iframe or window represented by a `WindowProxy` object to be ready to receive messages before resolving. For an iframe this is a case of waiting for the `load` event to fire. + + ::: + ### Step 2: Validate app & instance identity Apps and instance of them must identify themselves so that DAs can positively associate them with their corresponding AppD records and any existing instance identity. @@ -248,7 +292,7 @@ Desktop Agent Preload interfaces, as used in container-based Desktop Agent imple However, Browser Resident Desktop Agents working with a Desktop Agent Proxy interface may have more trouble tracking child windows and frames. Hence, a specific WCP message ([WCP6Goodbye](https://fdc3.finos.org/schemas/next/api/WCP6Goodbye.schema.json)) is provided for the `getAgent()` implementation to indicate that an app is disconnecting from the Desktop Agent and will not communicate further unless and until it reconnects via the WCP. The `getAgent()` implementation MUST listen for the `pagehide` event from the the HTML Standard's [Page Life Cycle API](https://wicg.github.io/page-lifecycle/spec.html) and send [WCP6Goodbye](https://fdc3.finos.org/schemas/next/api/WCP6Goodbye.schema.json) if it receives an event where the `persisted` property is `false`. -As it is possible for a page to close without firing this event in some circumstances, other procedures for detecting disconnection must also be used, these are described in the [Disconnects section of the Browser Resident Desktop Agents specification](./browserResidentDesktopAgents#disconnects). +As it is possible for a page to close without firing this event in some circumstances, other procedures for detecting disconnection may also be used, these are described in the [Browser Resident Desktop Agents specification](./browserResidentDesktopAgents#disconnects) and [Desktop Agent Communication Protocol](./desktopAgentCommunicationProtocol#checking-apps-are-alive). ### `getAgent()` Workflow Diagram @@ -258,67 +302,58 @@ The workflow defined in the Web Connection protocol for `getAgent()` is summariz --- title: "Web Connection Protocol Flowchart" --- -flowchart TD - A1([DesktopAgent launches app]) --> A2["App calls **getAgent()**"] - A2 --> A3["Check for **DesktopAgentDetails** in **SessionStorage**"] - - subgraph "getAgent() imeplementation" - A3 --> P1{"Does **window.fdc3** exist?"} - P1 ---|yes|P2["stop **timeout**"] - P2 --> P21["Save **DesktopAgentDetails** to **SessionStorage**"] - P1 ---|No|P3["Listen for **fdc3Ready**"] - P3 --> P31["**fdc3Ready event fires**"] - P31 --> P32["stop **timeout**"] - P32 --> P33["Save **DesktopAgentDetails** to **SessionStorage**"] +flowchart TB + A1([DesktopAgent launches app]) --> A2["App calls getAgent()"] + A2 --> A3 + + subgraph getAgent ["getAgent()"] + A3["Check for DesktopAgentDetails in SessionStorage"] --> P1{"Does window.fdc3 exist?"} + P1 -->|yes|P2["stop timeout"] + P2 --> P21["Save DesktopAgentDetails to SessionStorage"] + P1 -->|No|P3["Listen for fdc3Ready"] + P3 --> P31["fdc3Ready event fires"] + P31 --> P32["stop timeout"] + P32 --> P33["Save DesktopAgentDetails to SessionStorage"] A3 --> B1{"Do parent refs exist?"} - B1 --> B11["Send **WCP1Hello** to all candidates"] - B11 --> B2["Receive **WCP2LoadUrl**"] - B2 --> B21["stop **timeout**"] + B1 -->|yes|B11["Send WCP1Hello to all candidates"] + B11 --> B2["Receive WCP2LoadUrl"] + B2 --> B21["stop timeout"] B21 --> B22["Create hidden iframe with URL"] - B22 --> B23["Send **WCP1Hello** to iframe"] - B23 --> B3["Receive **WCP3Handshake** with **MessagePort**"] - B3 --> B31["stop **timeout**"] + B22 --> B23["await iframe's load event"] + B23 --> B24["Send WCP1Hello to iframe"] + B24 --> B3["Receive WCP3Handshake with MessagePort"] + B3 --> B31["stop timeout"] B11 --> B3 - B31 --> B32["Send **WCP4ValidateIdentity** on **MessagePort**"] - B32 --> B321["Receive **WCP5ValidateIdentityResponse**"] - B321 --> B3211["Create **DesktopAgentProxy** to process DACP messages"] - B3211 --> B3212["Save **DesktopAgentDetails** to **SessionStorage**"] - B32 --> B322["Receive **WCP5ValidateIdentityFailedResponse**"] + B31 --> B32["Send WCP4ValidateIdentity on MessagePort"] + B32 --> B321["Receive WCP5ValidateIdentityResponse"] + B321 --> B3211["Create DesktopAgentProxy to process DACP messages"] + B3211 --> B3212["Save DesktopAgentDetails to SessionStorage"] + B32 --> B322["Receive WCP5ValidateIdentityFailedResponse"] - A3 --> T1["Set **timeout**"] - T1 --> T2["**timeout** expires"] - T2 --> T3{"Was a **failover** fn provided"} - T3 ---|yes|T31["Run failover"] + A3 --> T1["Set timeout"] + T1 --> T2["timeout expires"] + T2 --> T3{"Was a failover fn provided"} + T3 -->|yes|T31["Run failover"] T31 --> T311{"Check failover return type"} - T311 ---|**WindowProxy**|T3111["Send **WCP1Hello** via **WindowProxy**"] - T311 ---|**DesktopAgent**|T3112["Save **DesktopAgentDetails** to **SessionStorage**"] + T311 -->|WindowProxy|T3111["Send WCP1Hello via WindowProxy"] + T311 -->|DesktopAgent|T3112["Save DesktopAgentDetails to SessionStorage"] T3111 --> B3 end - P21 -->P22(["resolve with **window.fdc3**"]) - P33 -->P34(["resolve with **window.fdc3**"]) - B3212 --> B3213(["resolve with **DesktopAgentProxy**"]) - B322 --> B3221(["reject with **AgentError.AccessDenied**"]) - T3112 --> T31121(["resolve with **DesktopAgent**"]) - T3 ---|no|T32(["reject with **AgentError.AgentNotFound**"]) - T311 ---|**Other**|T3113["reject with **AgentError.InvalidFailover**"] + P21 -->P22(["resolve with window.fdc3"]) + P33 -->P34(["resolve with window.fdc3"]) + B3212 --> B3213(["resolve with DesktopAgentProxy"]) + B322 --> B3221(["reject with AgentError.AccessDenied"]) + T3112 --> T31121(["resolve with DesktopAgent"]) + T3 -->|no|T32(["reject with AgentError.AgentNotFound"]) + T311 -->|other|T3113["reject with AgentError.InvalidFailover"] ``` ## Providing Channel Selector and Intent Resolver UIs Users of FDC3 Desktop Agents often need access to UI controls that allow them to select user channels or to resolve intents that have multiple resolution options. Whilst apps can implement these UIs on their own via data and API calls provided by the `DesktopAgent` API, Desktop Agents typically provide these interfaces themselves. -However, Browser Resident Desktop Agents may have difficulty displaying user interfaces over applications for a variety of reasons (inability to inject code, lack of permissions to display popups etc.), or may not (e.g. because they render applications in iframes within windows they control and can therefore display content over the iframe). The Web Connection Protocol and the `getAgent()` implementation based on it and incorporated into apps via the [@finos/fdc3 npm module](https://www.npmjs.com/package/@finos/fdc3), is intended to help Desktop Agents deliver these UIs where necessary. - -The WCP allows applications to indicate to the `getAgent()` implementation whether they need the UIs (they may not need one or the other based on their usage of the FDC3 API, or because they implement UIs themselves) and for Desktop Agents to provide custom implementations of them, or defer to reference implementations provided by the FDC3 Standard. This is achieved via: - -- **[WCP1Hello](https://fdc3.finos.org/schemas/next/api/WCP1Hello.schema.json)**: Sent by an application and incorporating boolean `payload.intentResolver` and `payload.channelSelector` fields, which are set to false if either UI is not needed (defaults to true). -- **[WCP3Handshake](https://fdc3.finos.org/schemas/next/api/WCP3Handshake.schema.json)**: Response sent by the Desktop Agent and incorporating `payload.intentResolverUrl` and `payload.channelSelectorUrl` fields, which should be set to the URL for each UI implementation, which should be loaded into an iframe to provide the UI (defaults to URLs for reference UI implementations provided by the FDC3 project). - -When UI iframes are created, the user interfaces may use messages incorporated into the [Desktop Agent Communication Protocol (DACP)](./desktopAgentCommunicationProtocol) to communicate with the `getAgent()` implementation and through it the Desktop Agent. - - -//TODO complete description once finalized. +However, Browser Resident Desktop Agents may have difficulty displaying user interfaces over applications for a variety of reasons (inability to inject code, lack of permissions to display popups etc.), or may not (e.g. because they render applications in iframes within windows they control and can therefore display content over the iframe). The Web Connection Protocol and the `getAgent()` implementation based on it and incorporated into apps via the [`@finos/fdc3` npm module](https://www.npmjs.com/package/@finos/fdc3), is intended to help Desktop Agents deliver these UIs where necessary. ```mermaid flowchart LR @@ -338,3 +373,10 @@ flowchart LR B-->cs B-->ir ``` + +The WCP allows applications to indicate to the `getAgent()` implementation whether they need the UIs (they may not need one or the other based on their usage of the FDC3 API, or because they implement UIs themselves) and for Desktop Agents to provide custom implementations of them, or defer to reference implementations provided by the FDC3 Standard. This is achieved via to following messages: + +- [`WCP1Hello`](https://fdc3.finos.org/schemas/next/api/WCP1Hello.schema.json): Sent by an application and incorporating boolean `payload.intentResolver` and `payload.channelSelector` fields, which are set to `false` if either UI is not needed (defaults to `true`). +- [`WCP3Handshake`](https://fdc3.finos.org/schemas/next/api/WCP3Handshake.schema.json): Response sent by the Desktop Agent and incorporating `payload.intentResolverUrl` and `payload.channelSelectorUrl` fields, which should be set to the URL for each UI implementation that should be loaded into an iframe to provide the UI (defaults to URLs for reference UI implementations provided by the FDC3 project), or set to `false` to indicate that the respective UI is not needed. Setting these fields to `true` will cause the `getAgent()` implementation to use its default URLs representing a reference implementation of each UI. + +When UI iframes are created, the user interfaces may use the 'iframe' messages incorporated into the [Desktop Agent Communication Protocol (DACP)](./desktopAgentCommunicationProtocol#controlling-injected-user-interfaces) to communicate with the `getAgent()` implementation and through it the Desktop Agent. diff --git a/package-lock.json b/package-lock.json index bc31ac77f..b8c03e886 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@kite9/fdc3", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@kite9/fdc3", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "license": "Apache-2.0", "workspaces": [ "packages/fdc3-schema", @@ -2591,8 +2591,8 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "version": "4.11.1", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -2744,8 +2744,8 @@ } }, "node_modules/@glideapps/ts-necessities": { - "version": "2.3.0", - "integrity": "sha512-3p4G89v4vU4A86Rf1QgXQk6nGG5nEffk9bFKmwn9k5J2m9lI8PHPClNChcqnZQjstztoeo98DwbOLIsCyvgGww==", + "version": "2.3.2", + "integrity": "sha512-tOXo3SrEeLu+4X2q6O2iNPXdGI1qoXEz/KrbkElTsWiWb69tFH4GzWz2K++0nBD6O3qO2Ft1C4L4ZvUfE2QDlQ==", "dev": true }, "node_modules/@humanwhocodes/config-array": { @@ -4302,8 +4302,8 @@ "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/qs": { - "version": "6.9.15", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "version": "6.9.16", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", "dev": true }, "node_modules/@types/range-parser": { @@ -6782,8 +6782,8 @@ "integrity": "sha512-5YM9LFQgVYfuLNEoqMqVWIBuF2UNCA+xu/jz1TyryLN/wmBcQSb+GNAwvLKvEpGESwgGN8XA1nbLAt6rKlyHYQ==" }, "node_modules/electron-to-chromium": { - "version": "1.5.20", - "integrity": "sha512-74mdl6Fs1HHzK9SUX4CKFxAtAe3nUns48y79TskHNAG6fGOlLfyKA4j855x+0b5u8rWJIrlaG9tcTPstMlwjIw==" + "version": "1.5.23", + "integrity": "sha512-mBhODedOXg4v5QWwl21DjM5amzjmI1zw9EPrPK/5Wx7C8jt33bpZNrC7OhHUG3pxRtbLpr3W2dXT+Ph1SsfRZA==" }, "node_modules/emittery": { "version": "0.13.1", @@ -7582,8 +7582,8 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.36.0", - "integrity": "sha512-c+RvVxBH0JE2kHt/8p043nPPhIohSnyQOZApIzGJqM2tXnjEzcZzyKIAg72gymLtuwuKfgGxW2H2aqTJqRgTfQ==", + "version": "7.36.1", + "integrity": "sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA==", "dev": true, "dependencies": { "array-includes": "^3.1.8", @@ -12312,8 +12312,8 @@ } }, "node_modules/postcss": { - "version": "8.4.45", - "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==", + "version": "8.4.47", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -12331,8 +12331,8 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -15110,8 +15110,8 @@ } }, "node_modules/vite": { - "version": "5.4.4", - "integrity": "sha512-RHFCkULitycHVTtelJ6jQLd+KSAAzOgEYorV32R2q++M6COBjKJR6BxqClwp5sf0XaBDjVMuJ9wnNfyAJwjMkA==", + "version": "5.4.5", + "integrity": "sha512-pXqR0qtb2bTwLkev4SE3r4abCNioP3GkjvIDLlzziPpXtHgiJIjuKl+1GN6ESOT3wMjG3JTeARopj2SwYaHTOA==", "dev": true, "dependencies": { "esbuild": "^0.21.3", @@ -15987,25 +15987,26 @@ } }, "packages/fdc3": { - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "license": "Apache-2.0", "dependencies": { - "@kite9/fdc3-context": "2.2.0-beta.16", - "@kite9/fdc3-get-agent": "2.2.0-beta.16", - "@kite9/fdc3-schema": "2.2.0-beta.16", - "@kite9/fdc3-standard": "2.2.0-beta.16" + "@kite9/fdc3-context": "2.2.0-beta.20", + "@kite9/fdc3-get-agent": "2.2.0-beta.20", + "@kite9/fdc3-schema": "2.2.0-beta.20", + "@kite9/fdc3-standard": "2.2.0-beta.20" } }, "packages/fdc3-agent-proxy": { - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", + "license": "Apache-2.0", "dependencies": { - "@kite9/fdc3-standard": "2.2.0-beta.16" + "@kite9/fdc3-standard": "2.2.0-beta.20" }, "devDependencies": { "@cucumber/cucumber": "10.3.1", "@cucumber/html-formatter": "11.0.4", "@cucumber/pretty-formatter": "1.0.1", - "@kite9/testing": "2.2.0-beta.16", + "@kite9/testing": "2.2.0-beta.20", "@types/expect": "24.3.0", "@types/lodash": "4.14.167", "@types/node": "^20.14.11", @@ -16030,7 +16031,7 @@ } }, "packages/fdc3-context": { - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "license": "Apache-2.0", "devDependencies": { "@types/jest": "29.5.12", @@ -16048,19 +16049,20 @@ } }, "packages/fdc3-get-agent": { - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", + "license": "Apache-2.0", "dependencies": { - "@kite9/fdc3-agent-proxy": "2.2.0-beta.16", - "@kite9/fdc3-context": "2.2.0-beta.16", - "@kite9/fdc3-schema": "2.2.0-beta.16", - "@kite9/fdc3-standard": "2.2.0-beta.16", + "@kite9/fdc3-agent-proxy": "2.2.0-beta.20", + "@kite9/fdc3-context": "2.2.0-beta.20", + "@kite9/fdc3-schema": "2.2.0-beta.20", + "@kite9/fdc3-standard": "2.2.0-beta.20", "@types/uuid": "^10.0.0", "uuid": "^9.0.1" }, "devDependencies": { "@cucumber/cucumber": "10.3.1", - "@kite9/fdc3-web-impl": "2.2.0-beta.16", - "@kite9/testing": "2.2.0-beta.16", + "@kite9/fdc3-web-impl": "2.2.0-beta.20", + "@kite9/testing": "2.2.0-beta.20", "@types/node": "^20.14.11", "@types/wtfnode": "^0.7.3", "expect": "^29.7.0", @@ -16073,7 +16075,7 @@ } }, "packages/fdc3-schema": { - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "license": "Apache-2.0", "devDependencies": { "@types/jest": "29.5.12", @@ -16091,11 +16093,11 @@ } }, "packages/fdc3-standard": { - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "license": "Apache-2.0", "dependencies": { - "@kite9/fdc3-context": "2.2.0-beta.16", - "@kite9/fdc3-schema": "2.2.0-beta.16" + "@kite9/fdc3-context": "2.2.0-beta.20", + "@kite9/fdc3-schema": "2.2.0-beta.20" }, "devDependencies": { "@types/jest": "29.5.12", @@ -16117,12 +16119,13 @@ } }, "packages/testing": { - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", + "license": "Apache-2.0", "dependencies": { "@cucumber/cucumber": "10.3.1", "@cucumber/html-formatter": "11.0.4", "@cucumber/pretty-formatter": "1.0.1", - "@kite9/fdc3-standard": "2.2.0-beta.16", + "@kite9/fdc3-standard": "2.2.0-beta.20", "@types/expect": "24.3.0", "@types/lodash": "4.14.167", "@types/node": "^20.14.11", @@ -16149,9 +16152,9 @@ } }, "toolbox/fdc3-for-web/demo": { - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "dependencies": { - "@kite9/fdc3": "2.2.0-beta.16", + "@kite9/fdc3": "2.2.0-beta.20", "@types/uuid": "^10.0.0", "@types/ws": "^8.5.10", "express": "^4.18.3", @@ -16171,9 +16174,10 @@ } }, "toolbox/fdc3-for-web/fdc3-web-impl": { - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", + "license": "Apache-2.0", "dependencies": { - "@kite9/fdc3-standard": "2.2.0-beta.16", + "@kite9/fdc3-standard": "2.2.0-beta.20", "@types/uuid": "^10.0.0", "uuid": "^9.0.1" }, @@ -16182,7 +16186,7 @@ "@cucumber/html-formatter": "11.0.4", "@cucumber/pretty-formatter": "1.0.1", "@kite9/fdc3-common": "2.2.0-beta.6", - "@kite9/testing": "2.2.0-beta.16", + "@kite9/testing": "2.2.0-beta.20", "@types/expect": "24.3.0", "@types/lodash": "4.14.167", "@types/node": "^20.14.11", @@ -16207,13 +16211,13 @@ } }, "toolbox/fdc3-workbench": { - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "license": "Apache-2.0", "devDependencies": { "@apidevtools/json-schema-ref-parser": "^9.0.9", "@fontsource/roboto": "^4.4.5", "@fontsource/source-code-pro": "^4.5.0", - "@kite9/fdc3": "2.2.0-beta.16", + "@kite9/fdc3": "2.2.0-beta.20", "@material-ui/core": "^4.11.4", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.59", diff --git a/package.json b/package.json index 99479e298..6693dd836 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/fdc3", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "author": "Fintech Open Source Foundation (FINOS)", "homepage": "https://fdc3.finos.org", "repository": { @@ -51,4 +51,4 @@ "devDependencies": { "concurrently": "^8.2.2" } -} \ No newline at end of file +} diff --git a/packages/fdc3-agent-proxy/package.json b/packages/fdc3-agent-proxy/package.json index 846f86897..eb64ae13b 100644 --- a/packages/fdc3-agent-proxy/package.json +++ b/packages/fdc3-agent-proxy/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/fdc3-agent-proxy", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "author": "Fintech Open Source Foundation (FINOS)", "homepage": "https://fdc3.finos.org", "repository": { @@ -22,7 +22,7 @@ "clean": "rimraf dist && rimraf cucumber-report.html && rimraf coverage && rimraf .nyc_output && rimraf node_modules" }, "dependencies": { - "@kite9/fdc3-standard": "2.2.0-beta.16" + "@kite9/fdc3-standard": "2.2.0-beta.20" }, "devDependencies": { "@cucumber/cucumber": "10.3.1", @@ -43,7 +43,7 @@ "is-ci": "2.0.0", "jsonpath-plus": "^9.0.0", "nyc": "15.1.0", - "@kite9/testing": "2.2.0-beta.16", + "@kite9/testing": "2.2.0-beta.20", "prettier": "3.2.5", "rimraf": "^6.0.1", "ts-node": "^10.9.2", @@ -51,4 +51,4 @@ "typescript": "^5.3.2", "uuid": "^9.0.1" } -} \ No newline at end of file +} diff --git a/packages/fdc3-agent-proxy/src/BasicDesktopAgent.ts b/packages/fdc3-agent-proxy/src/BasicDesktopAgent.ts index bab858927..a6487b078 100644 --- a/packages/fdc3-agent-proxy/src/BasicDesktopAgent.ts +++ b/packages/fdc3-agent-proxy/src/BasicDesktopAgent.ts @@ -1,9 +1,9 @@ -import { AppIdentifier, AppMetadata, ContextHandler, DesktopAgent, EventHandler, FDC3EventType, ImplementationMetadata, IntentHandler, IntentResolution, Listener } from "@kite9/fdc3-standard"; +import { AppIdentifier, AppMetadata, ContextHandler, DesktopAgent, EventHandler, FDC3EventTypes, ImplementationMetadata, IntentHandler, IntentResolution, Listener } from "@kite9/fdc3-standard"; import { ChannelSupport } from "./channels/ChannelSupport"; import { AppSupport } from "./apps/AppSupport"; import { IntentSupport } from "./intents/IntentSupport"; import { HandshakeSupport } from "./handshake/HandshakeSupport"; -import { DesktopAgentDetails, Connectable } from "@kite9/fdc3-standard"; +import { Connectable } from "@kite9/fdc3-standard"; import { Context } from "@kite9/fdc3-context"; /** @@ -26,15 +26,10 @@ export class BasicDesktopAgent implements DesktopAgent, Connectable { this.connectables = connectables } - addEventListener(_type: FDC3EventType | null, _handler: EventHandler): Promise { + addEventListener(_type: FDC3EventTypes | null, _handler: EventHandler): Promise { throw new Error("Method not implemented."); } - validateAppIdentity?({ }: { appId?: string; appDUrl?: string; instanceUuid?: string; }): Promise { - throw new Error("Method not implemented."); - } - - async getInfo(): Promise { return this.handshake.getImplementationMetadata() } diff --git a/packages/fdc3-agent-proxy/src/channels/DefaultPrivateChannel.ts b/packages/fdc3-agent-proxy/src/channels/DefaultPrivateChannel.ts index 3b24f2a43..4b8893784 100644 --- a/packages/fdc3-agent-proxy/src/channels/DefaultPrivateChannel.ts +++ b/packages/fdc3-agent-proxy/src/channels/DefaultPrivateChannel.ts @@ -1,4 +1,4 @@ -import { ContextHandler, Listener, PrivateChannel } from "@kite9/fdc3-standard"; +import { ContextHandler, EventHandler, Listener, PrivateChannel, PrivateChannelEventTypes } from "@kite9/fdc3-standard"; import { BrowserTypes } from "@kite9/fdc3-schema"; import { DefaultChannel } from "./DefaultChannel"; import { Messaging } from "../Messaging"; @@ -14,6 +14,10 @@ export class DefaultPrivateChannel extends DefaultChannel implements PrivateChan super(messaging, id, "private") } + addEventListener(_type: PrivateChannelEventTypes | null, _handler: EventHandler): Promise { + throw new Error("Method not implemented."); + } + onAddContextListener(handler: (contextType?: string | undefined) => void): Listener { const l = new PrivateChannelEventListenerType(this.messaging, this.id, "onAddContextListener", handler); l.register() diff --git a/packages/fdc3-agent-proxy/src/handshake/DefaultHandshakeSupport.ts b/packages/fdc3-agent-proxy/src/handshake/DefaultHandshakeSupport.ts index 8d4724180..f052e586c 100644 --- a/packages/fdc3-agent-proxy/src/handshake/DefaultHandshakeSupport.ts +++ b/packages/fdc3-agent-proxy/src/handshake/DefaultHandshakeSupport.ts @@ -1,23 +1,29 @@ +import { HeartbeatListener } from "../listeners/HeartbeatListener"; import { Messaging } from "../Messaging"; import { HandshakeSupport } from "./HandshakeSupport"; import { ImplementationMetadata } from "@kite9/fdc3-standard"; /** + * Handles connection, disconnection and heartbeats for the proxy. * This will possibly eventually need extending to allow for auth handshaking. */ export class DefaultHandshakeSupport implements HandshakeSupport { readonly messaging: Messaging + private heartbeatListener: HeartbeatListener | null = null constructor(messaging: Messaging) { this.messaging = messaging } async connect(): Promise { - return this.messaging.connect() + await this.messaging.connect() + this.heartbeatListener = new HeartbeatListener(this.messaging) + this.heartbeatListener.register() } async disconnect(): Promise { + this.heartbeatListener?.unsubscribe() return this.messaging.disconnect() } diff --git a/packages/fdc3-agent-proxy/src/listeners/HeartbeatListener.ts b/packages/fdc3-agent-proxy/src/listeners/HeartbeatListener.ts new file mode 100644 index 000000000..cb2d6ac22 --- /dev/null +++ b/packages/fdc3-agent-proxy/src/listeners/HeartbeatListener.ts @@ -0,0 +1,44 @@ +import { HeartbeatAcknowledgementRequest } from "@kite9/fdc3-schema/generated/api/BrowserTypes"; +import { Messaging } from "../Messaging"; +import { RegisterableListener } from "./RegisterableListener"; + +export class HeartbeatListener implements RegisterableListener { + + readonly id: string + readonly messaging: Messaging + + constructor(messaging: Messaging) { + this.id = "heartbeat-" + messaging.createUUID() + this.messaging = messaging + } + + filter(m: any): boolean { + return m.type === "heartbeatEvent" + + } + + action(_m: any): void { + this.messaging.post({ + + type: "heartbeatAcknowledgementRequest", + meta: { + requestUuid: this.messaging.createUUID(), + timestamp: new Date() + }, + payload: { + timestamp: new Date() + } + + } as HeartbeatAcknowledgementRequest) + console.log("Heartbeat acknowledged") + } + + async register(): Promise { + this.messaging.register(this) + } + + async unsubscribe(): Promise { + this.messaging.unregister(this.id) + } + +} \ No newline at end of file diff --git a/packages/fdc3-agent-proxy/test/features/heartbeat.feature b/packages/fdc3-agent-proxy/test/features/heartbeat.feature new file mode 100644 index 000000000..5299fa061 --- /dev/null +++ b/packages/fdc3-agent-proxy/test/features/heartbeat.feature @@ -0,0 +1,11 @@ +Feature: Heartbeats + + Background: Desktop Agent API + Given A Desktop Agent in "api" + And schemas loaded + + Scenario: Send A Heartbeat + When messaging receives a heartbeat event + And messaging will have posts + | matches_type | + | heartbeatAcknowledgement | diff --git a/packages/fdc3-agent-proxy/test/step-definitions/channels.steps.ts b/packages/fdc3-agent-proxy/test/step-definitions/channels.steps.ts index 48c21b597..3c3d3b104 100644 --- a/packages/fdc3-agent-proxy/test/step-definitions/channels.steps.ts +++ b/packages/fdc3-agent-proxy/test/step-definitions/channels.steps.ts @@ -1,5 +1,5 @@ import { DataTable, Given, Then, When } from '@cucumber/cucumber' -import { Context } from '@kite9/fdc3'; +import { Context } from '@kite9/fdc3-context'; import { handleResolve, matchData } from '@kite9/testing'; import { CustomWorld } from '../world/index'; import { BrowserTypes } from '@kite9/fdc3-schema'; diff --git a/packages/fdc3-agent-proxy/test/step-definitions/generic.steps.ts b/packages/fdc3-agent-proxy/test/step-definitions/generic.steps.ts index 2fc1b584e..d6a81f28a 100644 --- a/packages/fdc3-agent-proxy/test/step-definitions/generic.steps.ts +++ b/packages/fdc3-agent-proxy/test/step-definitions/generic.steps.ts @@ -1,9 +1,11 @@ import { TestMessaging } from '../support/TestMessaging'; -import { Given } from '@cucumber/cucumber' +import { Given, When } from '@cucumber/cucumber' import { CustomWorld } from '../world/index'; import { BasicDesktopAgent, DefaultAppSupport, DefaultChannelSupport, DefaultIntentSupport, DefaultHandshakeSupport } from '../../src'; import { SimpleIntentResolver, setupGenericSteps } from '@kite9/testing'; import { CHANNEL_STATE, SimpleChannelSelector } from '@kite9/testing/dist/src/agent'; +import { BrowserTypes } from '@kite9/fdc3-schema'; + Given('A Desktop Agent in {string}', async function (this: CustomWorld, field: string) { @@ -23,4 +25,16 @@ Given('A Desktop Agent in {string}', async function (this: CustomWorld, field: s this.props['result'] = null }) +When('messaging receives a heartbeat event', function (this: CustomWorld) { + + this.messaging?.receive({ + type: 'heartbeatEvent', + meta: this.messaging.createEventMeta(), + payload: { + timestamp: new Date() + } + } as BrowserTypes.HeartbeatEvent) + +}) + setupGenericSteps() \ No newline at end of file diff --git a/packages/fdc3-agent-proxy/test/step-definitions/intents.steps.ts b/packages/fdc3-agent-proxy/test/step-definitions/intents.steps.ts index 51c62d93c..7de42beef 100644 --- a/packages/fdc3-agent-proxy/test/step-definitions/intents.steps.ts +++ b/packages/fdc3-agent-proxy/test/step-definitions/intents.steps.ts @@ -2,7 +2,8 @@ import { Given } from '@cucumber/cucumber' import { CustomWorld } from '../world/index'; import { handleResolve } from '@kite9/testing'; import { BrowserTypes } from '@kite9/fdc3-schema'; -import { Context, ContextMetadata } from '@kite9/fdc3'; +import { Context } from '@kite9/fdc3-context'; +import { ContextMetadata } from '@kite9/fdc3-standard'; type IntentEvent = BrowserTypes.IntentEvent Given("app {string}", function (this: CustomWorld, appStr: string) { diff --git a/packages/fdc3-context/package.json b/packages/fdc3-context/package.json index 486bdf883..b7237e4bb 100644 --- a/packages/fdc3-context/package.json +++ b/packages/fdc3-context/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/fdc3-context", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "author": "Fintech Open Source Foundation (FINOS)", "homepage": "https://fdc3.finos.org", "repository": { diff --git a/packages/fdc3-get-agent/package.json b/packages/fdc3-get-agent/package.json index 9bc52882c..92dac303b 100644 --- a/packages/fdc3-get-agent/package.json +++ b/packages/fdc3-get-agent/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/fdc3-get-agent", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "author": "Fintech Open Source Foundation (FINOS)", "homepage": "https://fdc3.finos.org", "repository": { @@ -22,17 +22,17 @@ "clean": "rimraf dist && rimraf cucumber-report.html && rimraf coverage && rimraf .nyc_output && rimraf node_modules" }, "dependencies": { - "@kite9/fdc3-standard": "2.2.0-beta.16", - "@kite9/fdc3-agent-proxy": "2.2.0-beta.16", - "@kite9/fdc3-schema": "2.2.0-beta.16", - "@kite9/fdc3-context": "2.2.0-beta.16", + "@kite9/fdc3-standard": "2.2.0-beta.20", + "@kite9/fdc3-agent-proxy": "2.2.0-beta.20", + "@kite9/fdc3-schema": "2.2.0-beta.20", + "@kite9/fdc3-context": "2.2.0-beta.20", "@types/uuid": "^10.0.0", "uuid": "^9.0.1" }, "devDependencies": { "@cucumber/cucumber": "10.3.1", - "@kite9/fdc3-web-impl": "2.2.0-beta.16", - "@kite9/testing": "2.2.0-beta.16", + "@kite9/fdc3-web-impl": "2.2.0-beta.20", + "@kite9/testing": "2.2.0-beta.20", "@types/node": "^20.14.11", "@types/wtfnode": "^0.7.3", "expect": "^29.7.0", @@ -43,4 +43,4 @@ "typescript": "^5.3.2", "wtfnode": "^0.9.3" } -} \ No newline at end of file +} diff --git a/packages/fdc3-get-agent/src/index.ts b/packages/fdc3-get-agent/src/index.ts index 8b468faa9..1f74f5dbe 100644 --- a/packages/fdc3-get-agent/src/index.ts +++ b/packages/fdc3-get-agent/src/index.ts @@ -1,74 +1,12 @@ -import { DesktopAgent, GetAgentType, GetAgentParams } from '@kite9/fdc3-standard' -import { ElectronEventLoader } from './strategies/ElectronEventLoader' -import { handleWindowProxy, PostMessageLoader } from './strategies/PostMessageLoader' -import { TimeoutLoader } from './strategies/TimeoutLoader' - +import { DesktopAgent } from '@kite9/fdc3-standard' +import { getAgent } from './strategies/getAgent'; const DEFAULT_WAIT_FOR_MS = 20000; -export const FDC3_VERSION = "2.2" - -/** - * This return an FDC3 API. Should be called by application code. - * - * @param optionsOverride - options to override the default options - */ -export const getAgent: GetAgentType = (optionsOverride?: GetAgentParams) => { - - const DEFAULT_OPTIONS: GetAgentParams = { - dontSetWindowFdc3: true, - channelSelector: true, - intentResolver: true, - timeout: DEFAULT_WAIT_FOR_MS, - identityUrl: globalThis.window.location.href - } - - const options = { - ...DEFAULT_OPTIONS, - ...optionsOverride - } - - const STRATEGIES = [ - new ElectronEventLoader(), - new PostMessageLoader(), - new TimeoutLoader() - ] - - function handleGenericOptions(da: DesktopAgent) { - if ((!options.dontSetWindowFdc3) && (globalThis.window.fdc3 == null)) { - globalThis.window.fdc3 = da; - globalThis.window.dispatchEvent(new Event("fdc3Ready")); - } - - return da; - } - - const promises = STRATEGIES.map(s => s.get(options)); - - return Promise.race(promises) - .then(da => { - // first, cancel the timeout etc. - STRATEGIES.forEach(s => s.cancel()) - // either the timeout completes first with an error, or one of the other strategies completes with a DesktopAgent. - if (da) { - return da as DesktopAgent - } else { - throw new Error("No DesktopAgent found") - } - }) - .catch(async (error) => { - if (options.failover) { - const o = await handleWindowProxy(options, () => { return options.failover!!(options) }) - return o - } else { - throw error - } - }) - .then(da => handleGenericOptions(da)) -} +export { getAgent } /** - * Replaces the original fdc3Ready function from FDC3 2.0 with a new one that uses the new getClientAPI function. + * Replaces the original fdc3Ready function from FDC3 2.0 with a new one that uses the new getAgent function. * * @param waitForMs Amount of time to wait before failing the promise (20 seconds is the default). * @returns A DesktopAgent promise. diff --git a/packages/fdc3-get-agent/src/strategies/ElectronEventLoader.ts b/packages/fdc3-get-agent/src/strategies/ElectronEventLoader.ts index ad5a7983b..63a372ce7 100644 --- a/packages/fdc3-get-agent/src/strategies/ElectronEventLoader.ts +++ b/packages/fdc3-get-agent/src/strategies/ElectronEventLoader.ts @@ -2,8 +2,6 @@ import { DesktopAgent } from "@kite9/fdc3-standard"; import { GetAgentParams } from "@kite9/fdc3-standard"; import { Loader } from "./Loader"; - - /** * This approach will resolve the loader promise if the fdc3Ready event occurs. * This is done by electron implementations setting window.fdc3. diff --git a/packages/fdc3-get-agent/src/strategies/PostMessageLoader.ts b/packages/fdc3-get-agent/src/strategies/PostMessageLoader.ts index fc8b435b6..6a997e3e6 100644 --- a/packages/fdc3-get-agent/src/strategies/PostMessageLoader.ts +++ b/packages/fdc3-get-agent/src/strategies/PostMessageLoader.ts @@ -4,7 +4,7 @@ import { v4 as uuidv4 } from "uuid" import { ConnectionDetails } from '../messaging/MessagePortMessaging'; import { Loader } from './Loader'; import { BrowserTypes } from "@kite9/fdc3-schema"; -import { FDC3_VERSION } from '..'; +import { FDC3_VERSION } from './getAgent'; type WebConnectionProtocol1Hello = BrowserTypes.WebConnectionProtocol1Hello type WebConnectionProtocol2LoadURL = BrowserTypes.WebConnectionProtocol2LoadURL @@ -53,11 +53,23 @@ function sendWCP1Hello(w: MessageEventSource, options: GetAgentParams, connectio * The desktop agent requests that the client opens a URL in order to provide a message port. */ function openFrame(url: string): Window { + const IFRAME_ID = "fdc3-communications-embedded-iframe" + + // remove an old one if it's there + const existing = document.getElementById(IFRAME_ID) + if (existing) { + existing.remove() + } + + // create a new one var ifrm = document.createElement("iframe") ifrm.setAttribute("src", url) + ifrm.setAttribute("id", IFRAME_ID) ifrm.setAttribute("name", "FDC3 Communications") ifrm.style.width = "0px" ifrm.style.height = "0px" + ifrm.style.border = "0" + ifrm.style.position = "fixed" document.body.appendChild(ifrm) return ifrm.contentWindow!! } diff --git a/packages/fdc3-get-agent/src/strategies/getAgent.ts b/packages/fdc3-get-agent/src/strategies/getAgent.ts new file mode 100644 index 000000000..451d4b8ed --- /dev/null +++ b/packages/fdc3-get-agent/src/strategies/getAgent.ts @@ -0,0 +1,101 @@ +import { DesktopAgent, GetAgentType, GetAgentParams } from '@kite9/fdc3-standard' +import { ElectronEventLoader } from './ElectronEventLoader' +import { handleWindowProxy, PostMessageLoader } from './PostMessageLoader' +import { TimeoutLoader } from './TimeoutLoader' + +const DEFAULT_WAIT_FOR_MS = 20000; + +export const FDC3_VERSION = "2.2" + +/** + * For now, we only allow a single call to getAgent per application, so + * we keep track of the promise we use here. + */ +var theAgentPromise: Promise | null = null; + +export function clearAgentPromise() { + theAgentPromise = null; +} + +function getAgentPromise(options: GetAgentParams): Promise { + + const STRATEGIES = [ + new ElectronEventLoader(), + new PostMessageLoader(), + new TimeoutLoader() + ] + const promises = STRATEGIES.map(s => s.get(options)); + + return Promise.race(promises) + .then(da => { + // first, cancel the timeout etc. + STRATEGIES.forEach(s => s.cancel()) + + // either the timeout completes first with an error, or one of the other strategies completes with a DesktopAgent. + if (da) { + return da as DesktopAgent + } else { + throw new Error("No DesktopAgent found") + } + }) + .catch(async (error) => { + if (options.failover) { + const o = await handleWindowProxy(options, () => { return options.failover!!(options) }) + return o + } else { + throw error + } + }) +} + + +/** + * This return an FDC3 API. Should be called by application code. + * + * @param optionsOverride - options to override the default options + */ +export const getAgent: GetAgentType = (optionsOverride?: GetAgentParams) => { + + const DEFAULT_OPTIONS: GetAgentParams = { + dontSetWindowFdc3: true, + channelSelector: true, + intentResolver: true, + timeout: DEFAULT_WAIT_FOR_MS, + identityUrl: globalThis.window.location.href + } + + const options = { + ...DEFAULT_OPTIONS, + ...optionsOverride + } + + function handleGenericOptions(da: DesktopAgent) { + if ((!options.dontSetWindowFdc3) && (globalThis.window.fdc3 == null)) { + globalThis.window.fdc3 = da; + globalThis.window.dispatchEvent(new Event("fdc3Ready")); + } + + return da; + } + + if (!theAgentPromise) { + theAgentPromise = getAgentPromise(options).then(handleGenericOptions) + } + + return theAgentPromise +} + +/** + * Replaces the original fdc3Ready function from FDC3 2.0 with a new one that uses the new getAgent function. + * + * @param waitForMs Amount of time to wait before failing the promise (20 seconds is the default). + * @returns A DesktopAgent promise. + */ +export function fdc3Ready(waitForMs = DEFAULT_WAIT_FOR_MS): Promise { + return getAgent({ + timeout: waitForMs, + dontSetWindowFdc3: false, + channelSelector: true, + intentResolver: true + }) +} \ No newline at end of file diff --git a/packages/fdc3-get-agent/src/ui/DefaultDesktopAgentChannelSelector.ts b/packages/fdc3-get-agent/src/ui/DefaultDesktopAgentChannelSelector.ts index 4ea029c51..618d63764 100644 --- a/packages/fdc3-get-agent/src/ui/DefaultDesktopAgentChannelSelector.ts +++ b/packages/fdc3-get-agent/src/ui/DefaultDesktopAgentChannelSelector.ts @@ -10,7 +10,7 @@ type IframeChannelSelected = BrowserTypes.IframeChannelSelected * Works with the desktop agent to provide a simple channel selector. * * This is the default implementation, but can be overridden by app implementers calling - * the getAgentApi() method + * the getAgent() method */ export class DefaultDesktopAgentChannelSelector extends AbstractUIComponent implements ChannelSelector { diff --git a/packages/fdc3-get-agent/src/ui/DefaultDesktopAgentIntentResolver.ts b/packages/fdc3-get-agent/src/ui/DefaultDesktopAgentIntentResolver.ts index 3c245b43d..7b5b875bd 100644 --- a/packages/fdc3-get-agent/src/ui/DefaultDesktopAgentIntentResolver.ts +++ b/packages/fdc3-get-agent/src/ui/DefaultDesktopAgentIntentResolver.ts @@ -10,7 +10,7 @@ type IframeResolve = BrowserTypes.IframeResolve /** * Works with the desktop agent to provide a resolution to the intent choices. * This is the default implementation, but can be overridden by app implementers calling - * the getAgentApi() method + * the getAgent() method */ export class DefaultDesktopAgentIntentResolver extends AbstractUIComponent implements IntentResolver { diff --git a/packages/fdc3-get-agent/test/features/desktop-agent-strategy.feature b/packages/fdc3-get-agent/test/features/desktop-agent-strategy.feature index 17715443d..5fbeb1940 100644 --- a/packages/fdc3-get-agent/test/features/desktop-agent-strategy.feature +++ b/packages/fdc3-get-agent/test/features/desktop-agent-strategy.feature @@ -7,7 +7,7 @@ Feature: Different Strategies for Accessing the Desktop Agent Scenario: Running inside a Browser and using post message with direct message ports Given Parent Window desktop "da" listens for postMessage events in "{window}", returns direct message response And we wait for a period of "200" ms - And I call getAgentAPI for a promise result with the following options + And I call getAgent for a promise result with the following options | dontSetWindowFdc3 | timeout | intentResolver | channelSelector | | true | 8000 | false | false | And I refer to "{result}" as "theAPIPromise" @@ -30,7 +30,7 @@ Feature: Different Strategies for Accessing the Desktop Agent Scenario: Running inside a Browser using the embedded iframe strategy Given Parent Window desktop "da" listens for postMessage events in "{window}", returns iframe response And we wait for a period of "200" ms - And I call getAgentAPI for a promise result with the following options + And I call getAgent for a promise result with the following options | dontSetWindowFdc3 | timeout | | false | 8000 | And I refer to "{result}" as "theAPIPromise" @@ -65,10 +65,10 @@ Feature: Different Strategies for Accessing the Desktop Agent And I call "{desktopAgent}" with "disconnect" Scenario: Running inside an Electron Container. - In this scenario, window.fdc3 is set by the electron container and returned by getAgentAPI + In this scenario, window.fdc3 is set by the electron container and returned by getAgent Given A Dummy Desktop Agent in "dummy-api" - And I call getAgentAPI for a promise result + And I call fdc3Ready for a promise result And I refer to "{result}" as "theAPIPromise" And we wait for a period of "500" ms And `window.fdc3` is injected into the runtime with the value in "{dummy-api}" @@ -82,7 +82,7 @@ Feature: Different Strategies for Accessing the Desktop Agent Scenario: Failover Strategy returning desktop agent Given A Dummy Desktop Agent in "dummy-api" And "dummyFailover" is a function which returns a promise of "{dummy-api}" - And I call getAgentAPI for a promise result with the following options + And I call getAgent for a promise result with the following options | failover | timeout | | {dummyFailover} | 1000 | And I refer to "{result}" as "theAPIPromise" @@ -95,7 +95,7 @@ Feature: Different Strategies for Accessing the Desktop Agent Scenario: Failover Strategy returning a proxy Given "dummyFailover2" is a function which opens an iframe for communications on "{document}" - And I call getAgentAPI for a promise result with the following options + And I call getAgent for a promise result with the following options | failover | timeout | | {dummyFailover2} | 1000 | And I refer to "{result}" as "theAPIPromise" @@ -114,7 +114,7 @@ Feature: Different Strategies for Accessing the Desktop Agent And an existing app instance in "instanceID" And the session identity is set to "{instanceID}" And we wait for a period of "200" ms - And I call getAgentAPI for a promise result with the following options + And I call getAgent for a promise result with the following options | dontSetWindowFdc3 | timeout | intentResolver | channelSelector | | true | 8000 | false | false | And I refer to "{result}" as "theAPIPromise" @@ -128,10 +128,31 @@ Feature: Different Strategies for Accessing the Desktop Agent Given Parent Window desktop "da" listens for postMessage events in "{window}", returns direct message response And we wait for a period of "200" ms And the session identity is set to "BAD_INSTANCE" - And I call getAgentAPI for a promise result with the following options + And I call getAgent for a promise result with the following options | dontSetWindowFdc3 | timeout | intentResolver | channelSelector | | true | 8000 | false | false | And I refer to "{result}" as "theAPIPromise" Then the promise "{theAPIPromise}" should resolve And "{result}" is an error with message "Invalid instance" Then I call "{document}" with "shutdown" + + Scenario: Nothing works and we timeout + + Scenario: Someone calls getAgent twice + Given Parent Window desktop "da" listens for postMessage events in "{window}", returns direct message response + And we wait for a period of "200" ms + And I call getAgent for a promise result with the following options + | dontSetWindowFdc3 | timeout | intentResolver | channelSelector | + | true | 8000 | false | false | + And I refer to "{result}" as "theAPIPromise1" + And I call getAgent for a promise result with the following options + | dontSetWindowFdc3 | timeout | intentResolver | channelSelector | + | true | 8000 | false | false | + And I refer to "{result}" as "theAPIPromise2" + Then the promise "{theAPIPromise1}" should resolve + And I refer to "{result}" as "desktopAgent1" + And the promise "{theAPIPromise2}" should resolve + And I refer to "{result}" as "desktopAgent2" + And "{desktopAgent1}" is "{desktopAgent2}" + Then I call "{document}" with "shutdown" + And I call "{desktopAgent}" with "disconnect" diff --git a/packages/fdc3-get-agent/test/step-definitions/desktop-agent.steps.ts b/packages/fdc3-get-agent/test/step-definitions/desktop-agent.steps.ts index 46481c22d..2394e7ff6 100644 --- a/packages/fdc3-get-agent/test/step-definitions/desktop-agent.steps.ts +++ b/packages/fdc3-get-agent/test/step-definitions/desktop-agent.steps.ts @@ -2,12 +2,13 @@ import { After, DataTable, Given, When } from '@cucumber/cucumber' import { CustomWorld } from '../world'; import { handleResolve, setupGenericSteps } from '@kite9/testing'; import { MockDocument, MockWindow } from '../support/MockDocument'; -import { getAgent } from '../../src'; +import { fdc3Ready, getAgent } from '../../src'; import { DesktopAgentDetails, GetAgentParams, WebDesktopAgentType } from '@kite9/fdc3-standard'; import { dummyInstanceId, EMBED_URL, MockFDC3Server } from '../support/MockFDC3Server'; import { MockStorage } from '../support/MockStorage'; -import { DesktopAgent, ImplementationMetadata } from '@kite9/fdc3'; +import { DesktopAgent, ImplementationMetadata } from '@kite9/fdc3-standard'; import { DESKTOP_AGENT_SESSION_STORAGE_DETAILS_KEY } from '../../src/messaging/AbstractWebMessaging'; +import { clearAgentPromise } from '../../src/strategies/getAgent'; var wtf = require('wtfnode') setupGenericSteps() @@ -67,7 +68,7 @@ Given('`window.fdc3` is injected into the runtime with the value in {string}', a window.dispatchEvent(new Event('fdc3.ready')) }); -When('I call getAgentAPI for a promise result', function (this: CustomWorld) { +When('I call getAgent for a promise result', function (this: CustomWorld) { try { this.props['result'] = getAgent() } catch (error) { @@ -75,15 +76,25 @@ When('I call getAgentAPI for a promise result', function (this: CustomWorld) { } }) +When('I call fdc3Ready for a promise result', function (this: CustomWorld) { + try { + this.props['result'] = fdc3Ready() + } catch (error) { + this.props['result'] = error + } +}) + After(function (this: CustomWorld) { console.log("Cleaning up") + clearAgentPromise() setTimeout(() => { //console.log((process as any)._getActiveHandles()) wtf.dump() }, 10000) + }) -When('I call getAgentAPI for a promise result with the following options', function (this: CustomWorld, dt: DataTable) { +When('I call getAgent for a promise result with the following options', function (this: CustomWorld, dt: DataTable) { try { const first = dt.hashes()[0] const toArgs = Object.fromEntries(Object.entries(first) diff --git a/packages/fdc3-get-agent/test/support/MockDocument.ts b/packages/fdc3-get-agent/test/support/MockDocument.ts index 5ea67b403..874720732 100644 --- a/packages/fdc3-get-agent/test/support/MockDocument.ts +++ b/packages/fdc3-get-agent/test/support/MockDocument.ts @@ -43,6 +43,8 @@ export class MockElement { this.children.splice(this.children.indexOf(child), 1) } + remove() { + } } @@ -175,6 +177,10 @@ export class MockDocument { } } + getElementById(_id: string): HTMLElement | null { + return new MockElement("div") as any + } + body = new MockElement("body") shutdown() { diff --git a/packages/fdc3-get-agent/test/support/TestServerContext.ts b/packages/fdc3-get-agent/test/support/TestServerContext.ts index f7c062055..89494b984 100644 --- a/packages/fdc3-get-agent/test/support/TestServerContext.ts +++ b/packages/fdc3-get-agent/test/support/TestServerContext.ts @@ -1,6 +1,7 @@ import { ServerContext, InstanceID } from '@kite9/fdc3-web-impl' import { CustomWorld } from '../world' -import { OpenError, AppIdentifier, AppIntent, Context } from '@kite9/fdc3' +import { Context } from '@kite9/fdc3-context' +import { OpenError, AppIdentifier, AppIntent } from '@kite9/fdc3-standard' type ConnectionDetails = AppIdentifier & { msg?: object @@ -49,7 +50,7 @@ export class TestServerContext implements ServerContext { return this.instances.find(ca => ca.url === url) } - async disconnectApp(app: AppIdentifier): Promise { + async setAppDisconnected(app: AppIdentifier): Promise { this.instances = this.instances.filter(ca => ca.instanceId !== app.instanceId) } diff --git a/packages/fdc3-schema/generated/api/BrowserTypes.ts b/packages/fdc3-schema/generated/api/BrowserTypes.ts index 339f90dd2..c7e1a41b2 100644 --- a/packages/fdc3-schema/generated/api/BrowserTypes.ts +++ b/packages/fdc3-schema/generated/api/BrowserTypes.ts @@ -1,6 +1,6 @@ // To parse this data: // -// import { Convert, WebConnectionProtocol1Hello, WebConnectionProtocol2LoadURL, WebConnectionProtocol3Handshake, WebConnectionProtocol4ValidateAppIdentity, WebConnectionProtocol5ValidateAppIdentityFailedResponse, WebConnectionProtocol5ValidateAppIdentitySuccessResponse, WebConnectionProtocol6Goodbye, WebConnectionProtocolMessage, AddContextListenerRequest, AddContextListenerResponse, AddEventListenerEvent, AddEventListenerRequest, AddEventListenerResponse, AddIntentListenerRequest, AddIntentListenerResponse, AgentEventMessage, AgentResponseMessage, AppRequestMessage, BroadcastEvent, BroadcastRequest, BroadcastResponse, ChannelChangedEvent, ContextListenerUnsubscribeRequest, ContextListenerUnsubscribeResponse, CreatePrivateChannelRequest, CreatePrivateChannelResponse, EventListenerUnsubscribeRequest, EventListenerUnsubscribeResponse, FindInstancesRequest, FindInstancesResponse, FindIntentRequest, FindIntentResponse, FindIntentsByContextRequest, FindIntentsByContextResponse, GetAppMetadataRequest, GetAppMetadataResponse, GetCurrentChannelRequest, GetCurrentChannelResponse, GetCurrentContextRequest, GetCurrentContextResponse, GetInfoRequest, GetInfoResponse, GetOrCreateChannelRequest, GetOrCreateChannelResponse, GetUserChannelsRequest, GetUserChannelsResponse, HeartbeatAcknowledgementRequest, HeartbeatEvent, IframeChannelSelected, IframeChannels, IframeDrag, IframeHandshake, IframeHello, IframeMessage, IframeResolve, IframeResolveAction, IframeRestyle, IntentEvent, IntentListenerUnsubscribeRequest, IntentListenerUnsubscribeResponse, IntentResultRequest, IntentResultResponse, JoinUserChannelRequest, JoinUserChannelResponse, LeaveCurrentChannelRequest, LeaveCurrentChannelResponse, OpenRequest, OpenResponse, PrivateChannelDisconnectRequest, PrivateChannelDisconnectResponse, PrivateChannelOnAddContextListenerEvent, PrivateChannelOnDisconnectEvent, PrivateChannelOnUnsubscribeEvent, PrivateChannelUnsubscribeEventListenerRequest, PrivateChannelUnsubscribeEventListenerResponse, PrivateChannelAddEventListenerRequest, PrivateChannelAddEventListenerResponse, RaiseIntentForContextRequest, RaiseIntentForContextResponse, RaiseIntentRequest, RaiseIntentResponse, RaiseIntentResultResponse } from "./file"; +// import { Convert, WebConnectionProtocol1Hello, WebConnectionProtocol2LoadURL, WebConnectionProtocol3Handshake, WebConnectionProtocol4ValidateAppIdentity, WebConnectionProtocol5ValidateAppIdentityFailedResponse, WebConnectionProtocol5ValidateAppIdentitySuccessResponse, WebConnectionProtocol6Goodbye, WebConnectionProtocolMessage, AddContextListenerRequest, AddContextListenerResponse, AddEventListenerRequest, AddEventListenerResponse, AddIntentListenerRequest, AddIntentListenerResponse, AgentEventMessage, AgentResponseMessage, AppRequestMessage, BroadcastEvent, BroadcastRequest, BroadcastResponse, ChannelChangedEvent, ContextListenerUnsubscribeRequest, ContextListenerUnsubscribeResponse, CreatePrivateChannelRequest, CreatePrivateChannelResponse, EventListenerUnsubscribeRequest, EventListenerUnsubscribeResponse, FindInstancesRequest, FindInstancesResponse, FindIntentRequest, FindIntentResponse, FindIntentsByContextRequest, FindIntentsByContextResponse, GetAppMetadataRequest, GetAppMetadataResponse, GetCurrentChannelRequest, GetCurrentChannelResponse, GetCurrentContextRequest, GetCurrentContextResponse, GetInfoRequest, GetInfoResponse, GetOrCreateChannelRequest, GetOrCreateChannelResponse, GetUserChannelsRequest, GetUserChannelsResponse, HeartbeatAcknowledgementRequest, HeartbeatEvent, IframeChannelSelected, IframeChannels, IframeDrag, IframeHandshake, IframeHello, IframeMessage, IframeResolve, IframeResolveAction, IframeRestyle, IntentEvent, IntentListenerUnsubscribeRequest, IntentListenerUnsubscribeResponse, IntentResultRequest, IntentResultResponse, JoinUserChannelRequest, JoinUserChannelResponse, LeaveCurrentChannelRequest, LeaveCurrentChannelResponse, OpenRequest, OpenResponse, PrivateChannelDisconnectRequest, PrivateChannelDisconnectResponse, PrivateChannelOnAddContextListenerEvent, PrivateChannelOnDisconnectEvent, PrivateChannelOnUnsubscribeEvent, PrivateChannelUnsubscribeEventListenerRequest, PrivateChannelUnsubscribeEventListenerResponse, PrivateChannelAddEventListenerRequest, PrivateChannelAddEventListenerResponse, RaiseIntentForContextRequest, RaiseIntentForContextResponse, RaiseIntentRequest, RaiseIntentResponse, RaiseIntentResultResponse } from "./file"; // // const webConnectionProtocol1Hello = Convert.toWebConnectionProtocol1Hello(json); // const webConnectionProtocol2LoadURL = Convert.toWebConnectionProtocol2LoadURL(json); @@ -12,7 +12,6 @@ // const webConnectionProtocolMessage = Convert.toWebConnectionProtocolMessage(json); // const addContextListenerRequest = Convert.toAddContextListenerRequest(json); // const addContextListenerResponse = Convert.toAddContextListenerResponse(json); -// const addEventListenerEvent = Convert.toAddEventListenerEvent(json); // const addEventListenerRequest = Convert.toAddEventListenerRequest(json); // const addEventListenerResponse = Convert.toAddEventListenerResponse(json); // const addIntentListenerRequest = Convert.toAddIntentListenerRequest(json); @@ -747,68 +746,6 @@ export interface AddContextListenerResponsePayload { */ export type PurpleError = "AccessDenied" | "CreationFailed" | "MalformedContext" | "NoChannelFound"; -/** - * Identifies the type of the message and it is typically set to the FDC3 function name that - * the message relates to, e.g. 'findIntent', with 'Response' appended. - */ - -/** - * An event message from the Desktop Agent to an app for a specified event type. - * - * A message from a Desktop Agent to an FDC3-enabled app representing an event. - */ -export interface AddEventListenerEvent { - /** - * Metadata for messages sent by a Desktop Agent to an App notifying it of an event. - */ - meta: AddEventListenerEventMeta; - /** - * The message payload contains details of the event that the app is being notified about. - */ - payload: AddEventListenerEventPayload; - /** - * Identifies the type of the message and it is typically set to the FDC3 function name that - * the message relates to, e.g. 'findIntent', with 'Response' appended. - */ - type: "addEventListenerEvent"; -} - -/** - * Metadata for messages sent by a Desktop Agent to an App notifying it of an event. - */ -export interface AddEventListenerEventMeta { - eventUuid: string; - timestamp: Date; -} - -/** - * The message payload contains details of the event that the app is being notified about. - */ -export interface AddEventListenerEventPayload { - event: FDC3Event; -} - -/** - * An event object 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. - */ -export interface FDC3Event { - /** - * Additional details of the event, such as the `currentChannelId` for a CHANNEL_CHANGED - * event - */ - details: { [key: string]: any }; - type: "USER_CHANNEL_CHANGED"; -} - -/** - * The type of a (non-context and non-intent) event that may be received via the FDC3 API's - * addEventListener function. - * - * The type of the event to be listened to. - */ - /** * Identifies the type of the message and it is typically set to the FDC3 function name that * the message relates to, e.g. 'findIntent', with 'Response' appended. @@ -840,11 +777,16 @@ export interface AddEventListenerRequest { */ export interface AddEventListenerRequestPayload { /** - * The type of the event to be listened to. + * The type of the event to be listened to or `null` to listen to all event types. */ - type: "USER_CHANNEL_CHANGED"; + type: "USER_CHANNEL_CHANGED" | null; } +/** + * The type of a (non-context and non-intent) event that may be received via the FDC3 API's + * addEventListener function. + */ + /** * Identifies the type of the message and it is typically set to the FDC3 function name that * the message relates to, e.g. 'findIntent', with 'Request' appended. @@ -1121,7 +1063,7 @@ export interface BroadcastEvent { /** * Metadata for messages sent by a Desktop Agent to an App notifying it of an event. */ - meta: AddEventListenerEventMeta; + meta: BroadcastEventMeta; /** * The message payload contains details of the event that the app is being notified about. */ @@ -1133,6 +1075,14 @@ export interface BroadcastEvent { type: "broadcastEvent"; } +/** + * Metadata for messages sent by a Desktop Agent to an App notifying it of an event. + */ +export interface BroadcastEventMeta { + eventUuid: string; + timestamp: Date; +} + /** * The message payload contains details of the event that the app is being notified about. */ @@ -1310,7 +1260,7 @@ export interface ChannelChangedEvent { /** * Metadata for messages sent by a Desktop Agent to an App notifying it of an event. */ - meta: AddEventListenerEventMeta; + meta: BroadcastEventMeta; /** * The message payload contains details of the event that the app is being notified about. */ @@ -2380,7 +2330,7 @@ export interface HeartbeatEvent { /** * Metadata for messages sent by a Desktop Agent to an App notifying it of an event. */ - meta: AddEventListenerEventMeta; + meta: BroadcastEventMeta; /** * The message payload contains details of the event that the app is being notified about. */ @@ -2524,7 +2474,8 @@ export interface MouseOffsets { /** * Handshake message sent back to an iframe from the DA proxy code (setup by `getAgent()`) - * with a MessagePort appended over further communication is conducted. + * over the `MessagePort` provide in the preceding iFrameHello message, confirming that it + * is listening to the `MessagePort` for further communication. * * A message used to communicate with iframes injected by `getAgent()` for displaying UI * elements such as the intent resolver or channel selector. Used for messages sent in @@ -2557,7 +2508,8 @@ export interface IframeHandshakePayload { /** * Hello message sent by a UI iframe to the Desktop Agent proxy setup by `getAgent()` to - * indicate it is ready to communicate and containing initial CSS to set on the iframe. + * indicate it is ready to communicate, containing initial CSS to set on the iframe and + * including an appended `MessagePort` to be used for further communication. * * A message used to communicate with iframes injected by `getAgent()` for displaying UI * elements such as the intent resolver or channel selector. Used for messages sent in @@ -2602,11 +2554,11 @@ export interface InitialCSS { /** * The initial height of the iframe */ - height: string; + height?: string; /** * The initial left property to apply to the iframe */ - left: string; + left?: string; /** * The maximum height to apply to the iframe */ @@ -2622,7 +2574,7 @@ export interface InitialCSS { /** * The initial top property to apply to the iframe */ - top: string; + top?: string; /** * The transition property to apply to the iframe */ @@ -2630,7 +2582,7 @@ export interface InitialCSS { /** * The initial width of the iframe */ - width: string; + width?: string; /** * The initial zindex to apply to the iframe */ @@ -2830,7 +2782,7 @@ export interface IntentEvent { /** * Metadata for messages sent by a Desktop Agent to an App notifying it of an event. */ - meta: AddEventListenerEventMeta; + meta: BroadcastEventMeta; /** * The message payload contains details of the event that the app is being notified about. */ @@ -3330,7 +3282,7 @@ export interface PrivateChannelOnAddContextListenerEvent { /** * Metadata for messages sent by a Desktop Agent to an App notifying it of an event. */ - meta: AddEventListenerEventMeta; + meta: BroadcastEventMeta; /** * The message payload contains details of the event that the app is being notified about. */ @@ -3372,7 +3324,7 @@ export interface PrivateChannelOnDisconnectEvent { /** * Metadata for messages sent by a Desktop Agent to an App notifying it of an event. */ - meta: AddEventListenerEventMeta; + meta: BroadcastEventMeta; /** * The message payload contains details of the event that the app is being notified about. */ @@ -3409,7 +3361,7 @@ export interface PrivateChannelOnUnsubscribeEvent { /** * Metadata for messages sent by a Desktop Agent to an App notifying it of an event. */ - meta: AddEventListenerEventMeta; + meta: BroadcastEventMeta; /** * The message payload contains details of the event that the app is being notified about. */ @@ -3951,14 +3903,6 @@ export class Convert { return JSON.stringify(uncast(value, r("AddContextListenerResponse")), null, 2); } - public static toAddEventListenerEvent(json: string): AddEventListenerEvent { - return cast(JSON.parse(json), r("AddEventListenerEvent")); - } - - public static addEventListenerEventToJson(value: AddEventListenerEvent): string { - return JSON.stringify(uncast(value, r("AddEventListenerEvent")), null, 2); - } - public static toAddEventListenerRequest(json: string): AddEventListenerRequest { return cast(JSON.parse(json), r("AddEventListenerRequest")); } @@ -4826,29 +4770,13 @@ const typeMap: any = { { json: "error", js: "error", typ: u(undefined, r("PurpleError")) }, { json: "listenerUUID", js: "listenerUUID", typ: u(undefined, "") }, ], false), - "AddEventListenerEvent": o([ - { json: "meta", js: "meta", typ: r("AddEventListenerEventMeta") }, - { json: "payload", js: "payload", typ: r("AddEventListenerEventPayload") }, - { json: "type", js: "type", typ: r("AddEventListenerEventType") }, - ], false), - "AddEventListenerEventMeta": o([ - { json: "eventUuid", js: "eventUuid", typ: "" }, - { json: "timestamp", js: "timestamp", typ: Date }, - ], false), - "AddEventListenerEventPayload": o([ - { json: "event", js: "event", typ: r("FDC3Event") }, - ], false), - "FDC3Event": o([ - { json: "details", js: "details", typ: m("any") }, - { json: "type", js: "type", typ: r("FDC3EventType") }, - ], false), "AddEventListenerRequest": o([ { json: "meta", js: "meta", typ: r("AddContextListenerRequestMeta") }, { json: "payload", js: "payload", typ: r("AddEventListenerRequestPayload") }, { json: "type", js: "type", typ: r("AddEventListenerRequestType") }, ], false), "AddEventListenerRequestPayload": o([ - { json: "type", js: "type", typ: r("FDC3EventType") }, + { json: "type", js: "type", typ: u(r("FDC3EventType"), null) }, ], false), "AddEventListenerResponse": o([ { json: "meta", js: "meta", typ: r("AddContextListenerResponseMeta") }, @@ -4910,10 +4838,14 @@ const typeMap: any = { { json: "timestamp", js: "timestamp", typ: Date }, ], false), "BroadcastEvent": o([ - { json: "meta", js: "meta", typ: r("AddEventListenerEventMeta") }, + { json: "meta", js: "meta", typ: r("BroadcastEventMeta") }, { json: "payload", js: "payload", typ: r("BroadcastEventPayload") }, { json: "type", js: "type", typ: r("BroadcastEventType") }, ], false), + "BroadcastEventMeta": o([ + { json: "eventUuid", js: "eventUuid", typ: "" }, + { json: "timestamp", js: "timestamp", typ: Date }, + ], false), "BroadcastEventPayload": o([ { json: "channelId", js: "channelId", typ: u(null, "") }, { json: "context", js: "context", typ: r("Context") }, @@ -4942,7 +4874,7 @@ const typeMap: any = { { json: "error", js: "error", typ: u(undefined, r("ResponsePayloadError")) }, ], "any"), "ChannelChangedEvent": o([ - { json: "meta", js: "meta", typ: r("AddEventListenerEventMeta") }, + { json: "meta", js: "meta", typ: r("BroadcastEventMeta") }, { json: "payload", js: "payload", typ: r("ChannelChangedEventPayload") }, { json: "type", js: "type", typ: r("ChannelChangedEventType") }, ], false), @@ -5172,7 +5104,7 @@ const typeMap: any = { { json: "timestamp", js: "timestamp", typ: Date }, ], false), "HeartbeatEvent": o([ - { json: "meta", js: "meta", typ: r("AddEventListenerEventMeta") }, + { json: "meta", js: "meta", typ: r("BroadcastEventMeta") }, { json: "payload", js: "payload", typ: r("HeartbeatEventPayload") }, { json: "type", js: "type", typ: r("HeartbeatEventType") }, ], false), @@ -5222,14 +5154,14 @@ const typeMap: any = { ], false), "InitialCSS": o([ { json: "bottom", js: "bottom", typ: u(undefined, "") }, - { json: "height", js: "height", typ: "" }, - { json: "left", js: "left", typ: "" }, + { json: "height", js: "height", typ: u(undefined, "") }, + { json: "left", js: "left", typ: u(undefined, "") }, { json: "maxHeight", js: "maxHeight", typ: u(undefined, "") }, { json: "maxWidth", js: "maxWidth", typ: u(undefined, "") }, { json: "right", js: "right", typ: u(undefined, "") }, - { json: "top", js: "top", typ: "" }, + { json: "top", js: "top", typ: u(undefined, "") }, { json: "transition", js: "transition", typ: u(undefined, "") }, - { json: "width", js: "width", typ: "" }, + { json: "width", js: "width", typ: u(undefined, "") }, { json: "zIndex", js: "zIndex", typ: u(undefined, "") }, ], "any"), "IframeMessage": o([ @@ -5273,7 +5205,7 @@ const typeMap: any = { { json: "zIndex", js: "zIndex", typ: u(undefined, "") }, ], "any"), "IntentEvent": o([ - { json: "meta", js: "meta", typ: r("AddEventListenerEventMeta") }, + { json: "meta", js: "meta", typ: r("BroadcastEventMeta") }, { json: "payload", js: "payload", typ: r("IntentEventPayload") }, { json: "type", js: "type", typ: r("IntentEventType") }, ], false), @@ -5381,7 +5313,7 @@ const typeMap: any = { { json: "error", js: "error", typ: u(undefined, r("PurpleError")) }, ], false), "PrivateChannelOnAddContextListenerEvent": o([ - { json: "meta", js: "meta", typ: r("AddEventListenerEventMeta") }, + { json: "meta", js: "meta", typ: r("BroadcastEventMeta") }, { json: "payload", js: "payload", typ: r("PrivateChannelOnAddContextListenerEventPayload") }, { json: "type", js: "type", typ: r("PrivateChannelOnAddContextListenerEventType") }, ], false), @@ -5390,7 +5322,7 @@ const typeMap: any = { { json: "privateChannelId", js: "privateChannelId", typ: "" }, ], false), "PrivateChannelOnDisconnectEvent": o([ - { json: "meta", js: "meta", typ: r("AddEventListenerEventMeta") }, + { json: "meta", js: "meta", typ: r("BroadcastEventMeta") }, { json: "payload", js: "payload", typ: r("PrivateChannelOnDisconnectEventPayload") }, { json: "type", js: "type", typ: r("PrivateChannelOnDisconnectEventType") }, ], false), @@ -5398,7 +5330,7 @@ const typeMap: any = { { json: "privateChannelId", js: "privateChannelId", typ: "" }, ], false), "PrivateChannelOnUnsubscribeEvent": o([ - { json: "meta", js: "meta", typ: r("AddEventListenerEventMeta") }, + { json: "meta", js: "meta", typ: r("BroadcastEventMeta") }, { json: "payload", js: "payload", typ: r("PrivateChannelOnUnsubscribeEventPayload") }, { json: "type", js: "type", typ: r("PrivateChannelOnUnsubscribeEventType") }, ], false), @@ -5534,9 +5466,6 @@ const typeMap: any = { "FDC3EventType": [ "USER_CHANNEL_CHANGED", ], - "AddEventListenerEventType": [ - "addEventListenerEvent", - ], "AddEventListenerRequestType": [ "addEventListenerRequest", ], diff --git a/packages/fdc3-schema/package.json b/packages/fdc3-schema/package.json index b1173e302..a9112b78a 100644 --- a/packages/fdc3-schema/package.json +++ b/packages/fdc3-schema/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/fdc3-schema", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "author": "Fintech Open Source Foundation (FINOS)", "homepage": "https://fdc3.finos.org", "repository": { diff --git a/packages/fdc3-schema/schemas/api/addEventListenerEvent.schema.json b/packages/fdc3-schema/schemas/api/addEventListenerEvent.schema.json deleted file mode 100644 index 5807b54c8..000000000 --- a/packages/fdc3-schema/schemas/api/addEventListenerEvent.schema.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://fdc3.finos.org/schemas/next/api/addEventListenerEvent.schema.json", - "type": "object", - "title": "addEventListener Event", - "description": "An event message from the Desktop Agent to an app for a specified event type.", - "allOf": [ - { - "$ref": "agentEvent.schema.json" - }, - { - "type": "object", - "properties": { - "type": { - "$ref": "#/$defs/AddEventListenerEventType" - }, - "payload": { - "$ref": "#/$defs/AddEventListenerEventPayload" - }, - "meta": true - }, - "additionalProperties": false - } - ], - "$defs": { - "AddEventListenerEventType": { - "title": "AddEventListener Event Message Type", - "const": "addEventListenerEvent" - }, - "AddEventListenerEventPayload": { - "title": "addEventListener Event Payload", - "type": "object", - "properties": { - "event": { - "$ref": "api.schema.json#/definitions/FDC3Event" - } - }, - "additionalProperties": false, - "required": [ - "event" - ] - } - } -} \ No newline at end of file diff --git a/packages/fdc3-schema/schemas/api/addEventListenerRequest.schema.json b/packages/fdc3-schema/schemas/api/addEventListenerRequest.schema.json index 9a05aeef2..1417499d0 100644 --- a/packages/fdc3-schema/schemas/api/addEventListenerRequest.schema.json +++ b/packages/fdc3-schema/schemas/api/addEventListenerRequest.schema.json @@ -33,8 +33,15 @@ "properties": { "type": { "title": "Event type", - "description": "The type of the event to be listened to.", - "$ref": "api.schema.json#/definitions/FDC3EventType" + "description": "The type of the event to be listened to or `null` to listen to all event types.", + "oneOf": [ + { + "$ref": "api.schema.json#/definitions/FDC3EventType" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false, diff --git a/packages/fdc3-schema/schemas/api/iFrameHandshake.schema.json b/packages/fdc3-schema/schemas/api/iFrameHandshake.schema.json index 4e0b534a3..7d96825f3 100644 --- a/packages/fdc3-schema/schemas/api/iFrameHandshake.schema.json +++ b/packages/fdc3-schema/schemas/api/iFrameHandshake.schema.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://fdc3.finos.org/schemas/next/api/iframeHandshake.schema.json", "title": "iframe Handshake", - "description": "Handshake message sent back to an iframe from the DA proxy code (setup by `getAgent()`) with a MessagePort appended over further communication is conducted.", + "description": "Handshake message sent back to an iframe from the DA proxy code (setup by `getAgent()`) over the `MessagePort` provide in the preceding iFrameHello message, confirming that it is listening to the `MessagePort` for further communication.", "type": "object", "allOf": [ { diff --git a/packages/fdc3-schema/schemas/api/iFrameHello.schema.json b/packages/fdc3-schema/schemas/api/iFrameHello.schema.json index d3658f00b..78f736492 100644 --- a/packages/fdc3-schema/schemas/api/iFrameHello.schema.json +++ b/packages/fdc3-schema/schemas/api/iFrameHello.schema.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://fdc3.finos.org/schemas/next/api/iframeHello.schema.json", "title": "iframe Hello", - "description": "Hello message sent by a UI iframe to the Desktop Agent proxy setup by `getAgent()` to indicate it is ready to communicate and containing initial CSS to set on the iframe.", + "description": "Hello message sent by a UI iframe to the Desktop Agent proxy setup by `getAgent()` to indicate it is ready to communicate, containing initial CSS to set on the iframe and including an appended `MessagePort` to be used for further communication.", "type": "object", "allOf": [ { @@ -45,7 +45,7 @@ "maxHeight": {"type": "string", "title": "maxHeight", "description": "The maximum height to apply to the iframe"}, "maxWidth": {"type": "string", "title": "maxWidth", "description": "The maximum with to apply to the iframe"} }, - "required": ["height", "width", "top", "left"] + "required": [] } }, "additionalProperties": false, diff --git a/packages/fdc3-schema/schemas/api/raiseIntentResultResponse.schema.json b/packages/fdc3-schema/schemas/api/raiseIntentResultResponse.schema.json index f4f1419f3..4cde49972 100644 --- a/packages/fdc3-schema/schemas/api/raiseIntentResultResponse.schema.json +++ b/packages/fdc3-schema/schemas/api/raiseIntentResultResponse.schema.json @@ -49,6 +49,7 @@ }, "RaiseIntentResultErrorResponsePayload": { "title": "RaiseIntentResult Error Response Payload", + "type": "object", "properties": { "error": { "$ref": "common.schema.json#/$defs/ErrorMessages" diff --git a/packages/fdc3-standard/package.json b/packages/fdc3-standard/package.json index a07aafad7..fbbf45698 100644 --- a/packages/fdc3-standard/package.json +++ b/packages/fdc3-standard/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/fdc3-standard", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "author": "Fintech Open Source Foundation (FINOS)", "homepage": "https://fdc3.finos.org", "repository": { @@ -31,8 +31,8 @@ "printWidth": 120 }, "dependencies": { - "@kite9/fdc3-schema": "2.2.0-beta.14", - "@kite9/fdc3-context": "2.2.0-beta.14" + "@kite9/fdc3-schema": "2.2.0-beta.20", + "@kite9/fdc3-context": "2.2.0-beta.20" }, "devDependencies": { "@types/jest": "29.5.12", @@ -52,4 +52,4 @@ "tslib": "^2.0.1", "typescript": "^5.3.2" } -} \ No newline at end of file +} diff --git a/packages/fdc3-standard/src/api/DesktopAgent.ts b/packages/fdc3-standard/src/api/DesktopAgent.ts index bc679344c..34b497993 100644 --- a/packages/fdc3-standard/src/api/DesktopAgent.ts +++ b/packages/fdc3-standard/src/api/DesktopAgent.ts @@ -15,7 +15,7 @@ import { AppIdentifier } from './AppIdentifier'; import { AppMetadata } from './AppMetadata'; import { Intent } from '../intents/Intents'; import { ContextType } from '../context/ContextType'; -import { EventHandler, FDC3EventType } from './Events'; +import { EventHandler, FDC3EventTypes } from './Events'; /** * A Desktop Agent is a desktop component (or aggregate of components) that serves as a @@ -385,7 +385,7 @@ export interface DesktopAgent { * const listener = await fdc3.addEventListener(null, event => { ... }); * * // listener for a specific event type that logs its details - * const userChannelChangedListener = await fdc3.addEventListener(FDC3EventType.USER_CHANNEL_CHANGED, event => { + * const userChannelChangedListener = await fdc3.addEventListener("userChannelChanged", event => { * console.log(`Received event ${event.type}\n\tDetails: ${event.details}`); * //do something else with the event * }); @@ -395,7 +395,7 @@ export interface DesktopAgent { * @param {EventHandler} handler A function that events received will be passed to. * */ - addEventListener(type: FDC3EventType | null, handler: EventHandler): Promise; + addEventListener(type: FDC3EventTypes | null, handler: EventHandler): Promise; /** * Retrieves a list of the User channels available for the app to join. diff --git a/packages/fdc3-standard/src/api/Events.ts b/packages/fdc3-standard/src/api/Events.ts index e0f003dac..bf507d74f 100644 --- a/packages/fdc3-standard/src/api/Events.ts +++ b/packages/fdc3-standard/src/api/Events.ts @@ -3,36 +3,102 @@ * Copyright FINOS FDC3 contributors - see NOTICE file */ -/** Type representing a handler function for events from the Desktop Agent. - * @param {FDC3Event} event The handler function will be passed an `FDC3Event` Object - * providing details of the event (such as a change of channel membership for the app) as the only - * parameter. +/** + * Type defining a basic event object that may be emitted by an FDC3 API interface + * such as DesktopAgent or PrivateChannel. There are more specific event types + * defined for each interface. + */ +export interface ApiEvent { + readonly type: string; + readonly details: any; +} + +/** Type representing a handler function for events from the Desktop Agent + * or a PrivateChannel. + * @param event The handler function will be passed an `ApiEvent` (or more specifically + * an `FDC3Event` or `PrivateChannelEvent`) Object providing details of the event (such + * as a change of channel membership for the app, or type of context listener added) + * as the only parameter. */ -export type EventHandler = (event: FDC3Event) => void; +export type EventHandler = (event: ApiEvent ) => void; /** - * Enumeration defining the types of (non-context and non-intent) events that may be received - via the FDC3 API's `addEventListener` function. + * Type defining valid type strings for DesktopAgent interface events. */ -export enum FDC3EventType { - USER_CHANNEL_CHANGED = "USER_CHANNEL_CHANGED" -} +export type FDC3EventTypes = "userChannelChanged"; + /** - * Type representing the format of event objects that may be received - via the FDC3 API's `addEventListener` function. + * Type defining the format of event objects that may be received + * via the FDC3 API's `addEventListener` function. */ -export interface FDC3Event { - readonly type: FDC3EventType; +export interface FDC3Event extends ApiEvent { + readonly string: FDC3EventTypes; readonly details: any; } /** - * Type representing the format of event USER_CHANNEL_CHANGED objects + * Type defining the format of event `userChannelChanged` objects */ export interface FDC3ChannelChangedEvent extends FDC3Event { - readonly type: FDC3EventType.USER_CHANNEL_CHANGED; + readonly type: "userChannelChanged"; readonly details: { currentChannelId: string | null }; -} \ No newline at end of file +} + +/** + * Type defining valid type strings for Private Channel events. + */ +export type PrivateChannelEventTypes = "addContextListener" | "unsubscribe" | "disconnect"; + +/** + * Type defining the format of event objects that may be received + * via a PrivateChannel's `addEventListener` function. + */ +export interface PrivateChannelEvent { + readonly type: PrivateChannelEventTypes; + readonly details: any; +} + +/** + * 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. + */ +export interface PrivateChannelAddContextListenerEvent extends PrivateChannelEvent { + readonly type: "addContextListener"; + 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. + */ +export interface PrivateChannelUnsubscribeEvent extends PrivateChannelEvent { + readonly type: "unsubscribe"; + readonly details: { + contextType: string | null + }; +} + +/** + * Type defining the format of events representing a remote app being terminated + * or otherwise disconnecting from the PrivateChannel. This event is in addition to + * unsubscribe events that will also be fired for any context listeners they had added. + * No details are provided. + */ +export interface PrivateChannelDisconnectEvent extends PrivateChannelEvent { + readonly type: "disconnect"; + readonly details: null | undefined; +} diff --git a/packages/fdc3-standard/src/api/Listener.ts b/packages/fdc3-standard/src/api/Listener.ts index ced9e2e31..267da7e72 100644 --- a/packages/fdc3-standard/src/api/Listener.ts +++ b/packages/fdc3-standard/src/api/Listener.ts @@ -7,5 +7,5 @@ export interface Listener { /** * Unsubscribe the listener object. */ - unsubscribe(): void; + unsubscribe(): Promise; } diff --git a/packages/fdc3-standard/src/api/Methods.ts b/packages/fdc3-standard/src/api/Methods.ts index ee394e70c..ad6cdd5b5 100644 --- a/packages/fdc3-standard/src/api/Methods.ts +++ b/packages/fdc3-standard/src/api/Methods.ts @@ -17,7 +17,7 @@ import { StandardContextType, StandardIntent, ContextType, - FDC3EventType, + FDC3EventTypes, EventHandler, } from '..'; import { StandardContextsSet } from '../internal/contextConfiguration'; @@ -87,7 +87,7 @@ export function addContextListener( } } -export function addEventListener(eventType: FDC3EventType, handler: EventHandler): Promise { +export function addEventListener(eventType: FDC3EventTypes, handler: EventHandler): Promise { return rejectIfNoGlobal(() => window.fdc3.addEventListener(eventType, handler)); } diff --git a/packages/fdc3-standard/src/api/PrivateChannel.ts b/packages/fdc3-standard/src/api/PrivateChannel.ts index a2036b21a..f9a15fa09 100644 --- a/packages/fdc3-standard/src/api/PrivateChannel.ts +++ b/packages/fdc3-standard/src/api/PrivateChannel.ts @@ -5,6 +5,7 @@ import { Listener } from './Listener'; import { Channel } from './Channel'; +import { EventHandler, PrivateChannelEventTypes } from './Events'; /** * Object representing a private context channel, which is intended to support @@ -20,7 +21,47 @@ import { Channel } from './Channel'; * - MUST provide the `id` value for the channel as required by the Channel interface. */ export interface PrivateChannel extends 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. + * + * ```js + * // any event type + * const listener = await myPrivateChannel.addEventListener(null, event => { + * console.log(`Received event ${event.type}\n\tDetails: ${event.details}`); + * }); + * + * // listener for a specific event type + * const channelChangedListener = await myPrivateChannel.addEventListener( + * "addContextListener", + * event => { ... } + * ); + * ``` + * + * @param {PrivateChannelEventType | null} type If non-null, only events of the specified type will be received by the handler. + * @param {EventHandler} handler A function that events received will be passed to. + * + */ + addEventListener(type: PrivateChannelEventTypes | null, handler: EventHandler): Promise; + + /** + * 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. + */ + disconnect(): Promise; + + //--------------------------------------------------------------------------------------------- + //Deprecated function signatures + //--------------------------------------------------------------------------------------------- + /** + * @deprecated use `addEventListener("addContextListener", handler)` instead. + * * Adds a listener that will be called each time that the remote app invokes * addContextListener on this channel. * @@ -31,6 +72,8 @@ export interface PrivateChannel extends Channel { onAddContextListener(handler: (contextType?: string) => void): Listener; /** + * @deprecated use `addEventListener("unsubscribe", handler)` instead. + * * Adds a listener that will be called whenever the remote app invokes * Listener.unsubscribe() on a context listener that it previously added. * @@ -40,19 +83,11 @@ export interface PrivateChannel extends Channel { onUnsubscribe(handler: (contextType?: string) => void): Listener; /** + * @deprecated use `addEventListener("disconnect", handler)` instead. + * * 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. */ onDisconnect(handler: () => void): Listener; - - /** - * 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. - */ - disconnect(): void; } diff --git a/packages/fdc3-standard/test/Methods.test.ts b/packages/fdc3-standard/test/Methods.test.ts index 83d538f34..2d8546c48 100644 --- a/packages/fdc3-standard/test/Methods.test.ts +++ b/packages/fdc3-standard/test/Methods.test.ts @@ -7,7 +7,6 @@ import { compareVersionNumbers, ContextHandler, DesktopAgent, - fdc3Ready, findIntent, findIntentsByContext, getCurrentChannel, @@ -218,7 +217,7 @@ describe('test ES6 module', () => { test('addIntentListener should delegate to window.fdc3.addIntentListener', async () => { const intent = 'ViewChart'; - const handler: ContextHandler = _ => {}; + const handler: ContextHandler = _ => { }; await addIntentListener(intent, handler); @@ -228,8 +227,8 @@ describe('test ES6 module', () => { test('addContextListener should delegate to window.fdc3.addContextListener', async () => { const type = 'fdc3.instrument'; - const handler1: ContextHandler = _ => {}; - const handler2: ContextHandler = _ => {}; + const handler1: ContextHandler = _ => { }; + const handler2: ContextHandler = _ => { }; await addContextListener(type, handler1); await addContextListener(handler2); @@ -324,95 +323,7 @@ describe('test ES6 module', () => { }); }); - describe('fdc3Ready', () => { - let eventListeners: any; - beforeEach(() => { - jest.useFakeTimers({ - legacyFakeTimers: true - }); - - eventListeners = {}; - - window.addEventListener = jest.fn((event, callback) => { - eventListeners[event] = callback; - }); - }); - - afterEach(() => { - window.fdc3 = (undefined as unknown) as DesktopAgent; - }); - - afterAll(() => { - jest.useRealTimers(); - }) - - test('resolves immediately if `window.fdc3` is already defined', async () => { - // set fdc3 object and call fdc3Ready - window.fdc3 = mock(); - const promise = fdc3Ready(); - - expect(setTimeout).not.toHaveBeenCalled(); - expect(clearTimeout).not.toHaveBeenCalled(); - expect(eventListeners).not.toHaveProperty('fdc3Ready'); - await expect(promise).resolves.toBe(undefined); - }); - - test('waits for specified milliseconds', async () => { - const waitForMs = 1000; - - const promise = fdc3Ready(waitForMs); - - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), waitForMs); - - jest.advanceTimersByTime(waitForMs); - - await expect(promise).rejects.toEqual(TimeoutError); - }); - - test('waits for 5000 milliseconds by default', async () => { - const defaultWaitForMs = 5000; - - const promise = fdc3Ready(); - - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), defaultWaitForMs); - - jest.advanceTimersByTime(defaultWaitForMs); - - await expect(promise).rejects.toEqual(TimeoutError); - }); - - test('`fdc3Ready` event cancels timeout and rejects if `window.fdc3` is not defined', () => { - const promise = fdc3Ready(); - - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 5000); - expect(eventListeners).toHaveProperty('fdc3Ready'); - - // trigger fdc3Ready event without setting fdc3 object - eventListeners['fdc3Ready'](); - - expect(clearTimeout).toHaveBeenCalledTimes(1); - return expect(promise).rejects.toEqual(UnexpectedError); - }); - - test('`fdc3Ready` event cancels timeout and resolves if `window.fdc3` is defined', async () => { - const promise = fdc3Ready(); - - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 5000); - expect(eventListeners).toHaveProperty('fdc3Ready'); - - // set fdc3 object and trigger fdc3 ready event - window.fdc3 = mock(); - eventListeners['fdc3Ready'](); - - expect(clearTimeout).toHaveBeenCalledTimes(1); - await expect(promise).resolves.toBe(undefined); - }); - }); }); describe('test version comparison functions', () => { diff --git a/packages/fdc3/package.json b/packages/fdc3/package.json index d15ca5090..21c73d28d 100644 --- a/packages/fdc3/package.json +++ b/packages/fdc3/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/fdc3", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "author": "Fintech Open Source Foundation (FINOS)", "homepage": "https://fdc3.finos.org", "repository": { @@ -22,9 +22,9 @@ "test": "tsc" }, "dependencies": { - "@kite9/fdc3-standard": "2.2.0-beta.16", - "@kite9/fdc3-schema": "2.2.0-beta.16", - "@kite9/fdc3-get-agent": "2.2.0-beta.16", - "@kite9/fdc3-context": "2.2.0-beta.16" + "@kite9/fdc3-standard": "2.2.0-beta.20", + "@kite9/fdc3-schema": "2.2.0-beta.20", + "@kite9/fdc3-get-agent": "2.2.0-beta.20", + "@kite9/fdc3-context": "2.2.0-beta.20" } -} \ No newline at end of file +} diff --git a/packages/testing/package.json b/packages/testing/package.json index e8bc56312..223e3efd2 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/testing", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "author": "Fintech Open Source Foundation (FINOS)", "homepage": "https://fdc3.finos.org", "repository": { @@ -26,7 +26,7 @@ "@cucumber/cucumber": "10.3.1", "@cucumber/html-formatter": "11.0.4", "@cucumber/pretty-formatter": "1.0.1", - "@kite9/fdc3-standard": "2.2.0-beta.16", + "@kite9/fdc3-standard": "2.2.0-beta.20", "@types/expect": "24.3.0", "@types/lodash": "4.14.167", "@types/node": "^20.14.11", @@ -51,4 +51,4 @@ "typescript": "^5.3.2", "uuid": "^9.0.1" } -} \ No newline at end of file +} diff --git a/packages/testing/src/steps/generic.steps.ts b/packages/testing/src/steps/generic.steps.ts index 80d213f3a..b8f07ef24 100644 --- a/packages/testing/src/steps/generic.steps.ts +++ b/packages/testing/src/steps/generic.steps.ts @@ -90,6 +90,13 @@ export function setupGenericSteps() { expect(handleResolve(field, this)).toBeDefined() }) + Then('{string} is true', function (this: PropsWorld, field: string) { + expect(handleResolve(field, this)).toBeTruthy() + }) + + Then('{string} is false', function (this: PropsWorld, field: string) { + expect(handleResolve(field, this)).toBeFalsy() + }) Then('{string} is undefined', function (this: PropsWorld, field: string) { expect(handleResolve(field, this)).toBeUndefined() diff --git a/toolbox/fdc3-for-web/demo/package.json b/toolbox/fdc3-for-web/demo/package.json index 61b14a32e..acd853681 100644 --- a/toolbox/fdc3-for-web/demo/package.json +++ b/toolbox/fdc3-for-web/demo/package.json @@ -1,7 +1,7 @@ { "name": "@kite9/demo", "private": true, - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "scripts": { "dev": "nodemon -w src/server src/server/main.ts" }, @@ -13,7 +13,7 @@ "vite": "^5.2.0" }, "dependencies": { - "@kite9/fdc3": "2.2.0-beta.16", + "@kite9/fdc3": "2.2.0-beta.20", "@types/uuid": "^10.0.0", "@types/ws": "^8.5.10", "express": "^4.18.3", diff --git a/toolbox/fdc3-for-web/demo/src/client/apps/ag-grid.ts b/toolbox/fdc3-for-web/demo/src/client/apps/ag-grid.ts index 2e514a710..93977f808 100644 --- a/toolbox/fdc3-for-web/demo/src/client/apps/ag-grid.ts +++ b/toolbox/fdc3-for-web/demo/src/client/apps/ag-grid.ts @@ -118,7 +118,7 @@ const init = async () => { // Ignore anything else. if (context.type !== "fdc3.instrument") return; - const symbol = context.id.ticker; + const symbol = context.id?.ticker; // Show the symbol in the search box filterBox.value = symbol; // Apply a filter based on the symbol diff --git a/toolbox/fdc3-for-web/demo/src/client/apps/chartiq.ts b/toolbox/fdc3-for-web/demo/src/client/apps/chartiq.ts index 6d90c0c5e..7c6ef30a2 100644 --- a/toolbox/fdc3-for-web/demo/src/client/apps/chartiq.ts +++ b/toolbox/fdc3-for-web/demo/src/client/apps/chartiq.ts @@ -12,7 +12,7 @@ const init = async () => { await fdc3.joinUserChannel(channels[0].id) } - const stx: any = window.stxx; + const stx: any = (window as any).stxx; // If the user changes the symbol, broadcast the new symbol stx.callbacks.symbolChange = () => { @@ -26,12 +26,12 @@ const init = async () => { // Listen for changes to fdc3.instrument, and update the symbol fdc3.addContextListener("fdc3.instrument", (context) => { - stx.newChart(context.id.ticker); + stx.newChart(context.id?.ticker); }); // Listen for ViewChart events fdc3.addIntentListener("ViewChart", (context) => { - stx.newChart(context.id.ticker); + stx.newChart(context.id?.ticker); }) }; diff --git a/toolbox/fdc3-for-web/demo/src/client/da/DemoServerContext.ts b/toolbox/fdc3-for-web/demo/src/client/da/DemoServerContext.ts index fc3b5cbbd..86e5b3c9a 100644 --- a/toolbox/fdc3-for-web/demo/src/client/da/DemoServerContext.ts +++ b/toolbox/fdc3-for-web/demo/src/client/da/DemoServerContext.ts @@ -63,6 +63,13 @@ export class DemoServerContext implements ServerContext { } } + async setAppDisconnected(app: AppIdentifier): Promise { + const idx = this.connections.findIndex(i => i.instanceId == app.instanceId) + if (idx != -1) { + this.connections.splice(idx, 1) + } + } + getOpener(): Opener { const cb = document.getElementById("opener") as HTMLInputElement; const val = cb.value @@ -132,7 +139,8 @@ export class DemoServerContext implements ServerContext { instanceId, window, url, - state: State.Pending + state: State.Pending, + lastHeartbeat: Date.now() } this.setInstanceDetails(instanceId, metadata) diff --git a/toolbox/fdc3-for-web/demo/src/client/da/dummy-desktop-agent.ts b/toolbox/fdc3-for-web/demo/src/client/da/dummy-desktop-agent.ts index 56797f401..7a5836b0d 100644 --- a/toolbox/fdc3-for-web/demo/src/client/da/dummy-desktop-agent.ts +++ b/toolbox/fdc3-for-web/demo/src/client/da/dummy-desktop-agent.ts @@ -55,8 +55,8 @@ window.addEventListener("load", () => { socket.emit(DA_HELLO, desktopAgentUUID) const directory = new FDC3_2_1_JSONDirectory() - //await directory.load("/static/da/appd.json") - await directory.load("/static/da/local-conformance-2_0.v2.json") + await directory.load("/static/da/appd.json") + //await directory.load("/static/da/local-conformance-2_0.v2.json") const sc = new DemoServerContext(socket, directory) const channelDetails: ChannelState[] = [ @@ -64,7 +64,7 @@ window.addEventListener("load", () => { { id: "two", type: ChannelType.user, context: [], displayMetadata: { name: "THE BLUE CHANNEL", color: "blue" } }, { id: "three", type: ChannelType.user, context: [], displayMetadata: { name: "THE GREEN CHANNEL", color: "green" } } ] - const fdc3Server = new DefaultFDC3Server(sc, directory, channelDetails) + const fdc3Server = new DefaultFDC3Server(sc, directory, channelDetails, true) socket.on(FDC3_APP_EVENT, (msg, from) => { fdc3Server.receive(msg, from) @@ -130,25 +130,5 @@ window.addEventListener("load", () => { - // const channelSelector: ChannelSelectorDetails | null = (getUi() == UI.DEMO) ? { - // uri: window.location.origin + "/static/da/channel-selector.html", - // collapsedCss: { - // width: "45px", - // height: "45px", - // right: "20px", - // bottom: "20px", - // position: "fixed" - // }, - // expandedCss: { - // width: "300px", - // height: "300px", - // right: "20px", - // bottom: "20px", - // position: "fixed" - // } - // } : null - - - }) diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/package.json b/toolbox/fdc3-for-web/fdc3-web-impl/package.json index b120e003e..29b26e6dd 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/package.json +++ b/toolbox/fdc3-for-web/fdc3-web-impl/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/fdc3-web-impl", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "author": "Fintech Open Source Foundation (FINOS)", "homepage": "https://fdc3.finos.org", "repository": { @@ -23,7 +23,7 @@ "build": "npm run directory-openapi && tsc --module es2022" }, "dependencies": { - "@kite9/fdc3-standard": "2.2.0-beta.16", + "@kite9/fdc3-standard": "2.2.0-beta.20", "@types/uuid": "^10.0.0", "uuid": "^9.0.1" }, @@ -32,7 +32,7 @@ "@cucumber/html-formatter": "11.0.4", "@cucumber/pretty-formatter": "1.0.1", "@kite9/fdc3-common": "2.2.0-beta.6", - "@kite9/testing": "2.2.0-beta.16", + "@kite9/testing": "2.2.0-beta.20", "@types/expect": "24.3.0", "@types/lodash": "4.14.167", "@types/node": "^20.14.11", @@ -55,4 +55,4 @@ "typescript": "^5.3.2", "uuid": "^9.0.1" } -} \ No newline at end of file +} diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/src/BasicFDC3Server.ts b/toolbox/fdc3-for-web/fdc3-web-impl/src/BasicFDC3Server.ts index 05580fbb3..4c1107a6c 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/src/BasicFDC3Server.ts +++ b/toolbox/fdc3-for-web/fdc3-web-impl/src/BasicFDC3Server.ts @@ -5,6 +5,7 @@ import { IntentHandler } from "./handlers/IntentHandler"; import { Directory } from "./directory/DirectoryInterface"; import { OpenHandler } from "./handlers/OpenHandler"; import { BrowserTypes } from "@kite9/fdc3-schema"; +import { HeartbeatHandler } from "./handlers/HeartbeatHandler"; type AppRequestMessage = BrowserTypes.AppRequestMessage type WebConnectionProtocol4ValidateAppIdentity = BrowserTypes.WebConnectionProtocol4ValidateAppIdentity @@ -15,6 +16,8 @@ export interface MessageHandler { * Handles an AgentRequestMessage from the messaging source */ accept(msg: any, sc: ServerContext, from: InstanceID): void + + shutdown(): void } /** @@ -34,15 +37,25 @@ export class BasicFDC3Server implements FDC3Server { // this.sc.log(`MessageReceived: \n ${JSON.stringify(message, null, 2)}`) this.handlers.forEach(h => h.accept(message, this.sc, from)) } + + shutdown(): void { + this.handlers.forEach(h => h.shutdown()) + } } export class DefaultFDC3Server extends BasicFDC3Server { - constructor(sc: ServerContext, directory: Directory, userChannels: ChannelState[], intentTimeoutMs: number = 20000, openHandlerTimeoutMs: number = 3000) { - super([ + constructor(sc: ServerContext, directory: Directory, userChannels: ChannelState[], heartbeats: boolean, intentTimeoutMs: number = 20000, openHandlerTimeoutMs: number = 3000) { + const handlers: MessageHandler[] = [ new BroadcastHandler(userChannels), new IntentHandler(directory, intentTimeoutMs), - new OpenHandler(directory, openHandlerTimeoutMs) - ], sc) + new OpenHandler(directory, openHandlerTimeoutMs), + ] + + if (heartbeats) { + handlers.push(new HeartbeatHandler(openHandlerTimeoutMs / 4, openHandlerTimeoutMs / 2, openHandlerTimeoutMs)) + } + + super(handlers, sc) } } \ No newline at end of file diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/src/ServerContext.ts b/toolbox/fdc3-for-web/fdc3-web-impl/src/ServerContext.ts index 744ad6922..d0bbfd2fb 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/src/ServerContext.ts +++ b/toolbox/fdc3-for-web/fdc3-web-impl/src/ServerContext.ts @@ -52,6 +52,11 @@ export interface ServerContext { */ setAppConnected(app: AppIdentifier): Promise + /** + * Unregisters the app as connected to the desktop agent. + */ + setAppDisconnected(app: AppIdentifier): Promise + /** * Returns the list of apps open and connected to FDC3 at the current time. * Note, it is the implementor's job to ensure this list is diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/BroadcastHandler.ts b/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/BroadcastHandler.ts index 3090c753b..b74774099 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/BroadcastHandler.ts +++ b/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/BroadcastHandler.ts @@ -60,6 +60,9 @@ export class BroadcastHandler implements MessageHandler { this.state = initialChannelState } + shutdown(): void { + } + getCurrentChannel(from: AppIdentifier): ChannelState | null { return this.currentChannel[from.instanceId!!] } diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/HeartbeatHandler.ts b/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/HeartbeatHandler.ts new file mode 100644 index 000000000..aeae2b2a8 --- /dev/null +++ b/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/HeartbeatHandler.ts @@ -0,0 +1,83 @@ +import { AppIdentifier } from "@kite9/fdc3-standard"; +import { MessageHandler } from "../BasicFDC3Server"; +import { InstanceID, ServerContext } from "../ServerContext"; +import { HeartbeatEvent } from "@kite9/fdc3-schema/generated/api/BrowserTypes"; + +/* + * Handles heartbeat pings and responses + */ +export class HeartbeatHandler implements MessageHandler { + + private readonly contexts: ServerContext[] = [] + private readonly lastHeartbeats: Map = new Map() + private readonly warnings: Set = new Set() + private readonly timeerFunction: NodeJS.Timeout + + constructor(pingInterval: number = 1000, warnAfter: number = 5000, deadAfter: number = 10000) { + + + this.timeerFunction = setInterval(() => { + this.contexts.forEach(async (sc) => { + (await sc.getConnectedApps()).forEach(app => { + const now = new Date().getTime() + this.sendHeartbeat(sc, app) + + // check when the last heartbeat happened + const lastHeartbeat = this.lastHeartbeats.get(app.instanceId!!) + + if (lastHeartbeat != undefined) { + const timeSinceLastHeartbeat = now - lastHeartbeat + + if (timeSinceLastHeartbeat < warnAfter) { + this.warnings.delete(app.instanceId!!) + } else if ((timeSinceLastHeartbeat > warnAfter) && (!this.warnings.has(app.instanceId!!))) { + console.warn(`No heartbeat from ${app.instanceId} for ${timeSinceLastHeartbeat}ms`) + this.warnings.add(app.instanceId!!) + } else if (timeSinceLastHeartbeat > deadAfter) { + console.error(`No heartbeat from ${app.instanceId} for ${timeSinceLastHeartbeat}ms. App is considered dead.`) + sc.setAppDisconnected(app) + } else { + // no action + } + + } else { + // start the clock + this.lastHeartbeats.set(app.instanceId!!, now) + } + }) + }) + }, pingInterval) + } + + shutdown(): void { + clearInterval(this.timeerFunction) + } + + accept(msg: any, sc: ServerContext, from: InstanceID): void { + if (!this.contexts.includes(sc)) { + this.contexts.push(sc) + } + + if (msg.type == 'heartbeatAcknowledgementRequest') { + const app = sc.getInstanceDetails(from) + if (app) { + this.lastHeartbeats.set(app.instanceId!!, new Date().getTime()) + } + } + } + + + async sendHeartbeat(sc: ServerContext, app: AppIdentifier): Promise { + sc.post({ + type: 'heartbeatEvent', + meta: { + timestamp: new Date(), + eventUuid: sc.createUUID(), + }, + payload: { + timestamp: new Date() + } + } as HeartbeatEvent, app.instanceId!!) + } + +} diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/IntentHandler.ts b/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/IntentHandler.ts index a66581915..1a26c1196 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/IntentHandler.ts +++ b/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/IntentHandler.ts @@ -92,6 +92,7 @@ class PendingIntent { }, ih.timeoutMs) } + async accept(arg0: ListenerRegistration): Promise { if ((arg0.appId == this.expectingAppId) && (arg0.intentName == this.r.intent)) { this.complete = true @@ -114,6 +115,9 @@ export class IntentHandler implements MessageHandler { this.timeoutMs = timeoutMs } + shutdown(): void { + } + async narrowIntents(appIntents: AppIntent[], context: Context, sc: ServerContext): Promise { return sc.narrowIntents(appIntents, context) } @@ -240,17 +244,13 @@ export class IntentHandler implements MessageHandler { const requestsWithListeners = arg0.filter(r => this.hasListener(target.instanceId!!, r.intent)) if (requestsWithListeners.length == 0) { - // intent not handled (no listener registered) - return errorResponseId(sc, arg0[0].requestUuid, arg0[0].from, ResolveError.NoAppsFound, arg0[0].type) - } - - if (requestsWithListeners.length == 1) { + // maybe listener hasn't been registered yet - create a pending intent + const pi = new PendingIntent(arg0[0], sc, this, target?.appId!!) + this.pendingIntents.add(pi) + } else { // ok, deliver to the current running app. return forwardRequest(requestsWithListeners[0], target, sc, this) } - - // in this case, we are raisingIntentForContext, and there are multiple listeners on this instance - return successResponseId(sc, arg0[0].requestUuid, arg0[0].from, { appIntents: this.createAppIntents(requestsWithListeners, [target]) }, arg0[0].type) } async raiseIntentRequestToSpecificAppId(arg0: IntentRequest[], sc: ServerContext, target: AppIdentifier): Promise { diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/OpenHandler.ts b/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/OpenHandler.ts index 7df114ed1..c386fb0be 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/OpenHandler.ts +++ b/toolbox/fdc3-for-web/fdc3-web-impl/src/handlers/OpenHandler.ts @@ -83,6 +83,9 @@ export class OpenHandler implements MessageHandler { this.timeoutMs = timeoutMs } + shutdown(): void { + } + async accept(msg: any, sc: ServerContext, uuid: InstanceID): Promise { switch (msg.type as string) { case 'addContextListenerRequest': return this.handleAddContextListener(msg as AddContextListenerRequest, sc, uuid) diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/test/features/heartbeat.feature b/toolbox/fdc3-for-web/fdc3-web-impl/test/features/heartbeat.feature new file mode 100644 index 000000000..b9522eaf5 --- /dev/null +++ b/toolbox/fdc3-for-web/fdc3-web-impl/test/features/heartbeat.feature @@ -0,0 +1,50 @@ +Feature: Heartbeat Messages Between Apps and Server + + Background: + Given schemas loaded + And "libraryApp" is an app with the following intents + | Intent Name | Context Type | Result Type | + | returnBook | fdc3.book | {empty} | + And A newly instantiated FDC3 Server with heartbeat checking + + Scenario: App Responds to heartbeats + When "libraryApp/a1" is opened with connection id "a1" + And "a1" sends validate + And we wait for a period of "500" ms + And "libraryApp/a1" sends a heartbeat response + And we wait for a period of "500" ms + And "libraryApp/a1" sends a heartbeat response + And we wait for a period of "500" ms + And "libraryApp/a1" sends a heartbeat response + And we wait for a period of "500" ms + And "libraryApp/a1" sends a heartbeat response + And we wait for a period of "500" ms + And "libraryApp/a1" sends a heartbeat response + And we wait for a period of "500" ms + Then I test the liveness of "libraryApp/a1" + Then "{result}" is true + And messaging will have outgoing posts + | msg.matches_type | to.instanceId | to.appId | + | heartbeatEvent | a1 | libraryApp | + | heartbeatEvent | a1 | libraryApp | + | heartbeatEvent | a1 | libraryApp | + | heartbeatEvent | a1 | libraryApp | + | heartbeatEvent | a1 | libraryApp | + | heartbeatEvent | a1 | libraryApp | + And I shutdown the server + + Scenario: App Doesn't Respond to heartbeats +Apps are considered dead if they don't respond to a heartbeat request within 2 seconds + + When "libraryApp/a1" is opened with connection id "a1" + And "a1" sends validate + And we wait for a period of "3000" ms + Then I test the liveness of "libraryApp/a1" + Then "{result}" is false + And messaging will have outgoing posts + | msg.matches_type | to.instanceId | to.appId | + | heartbeatEvent | a1 | libraryApp | + | heartbeatEvent | a1 | libraryApp | + | heartbeatEvent | a1 | libraryApp | + | heartbeatEvent | a1 | libraryApp | + And I shutdown the server diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/test/features/intent-result.feature b/toolbox/fdc3-for-web/fdc3-web-impl/test/features/intent-result.feature index 76bc227e1..0c4446959 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/test/features/intent-result.feature +++ b/toolbox/fdc3-for-web/fdc3-web-impl/test/features/intent-result.feature @@ -5,26 +5,40 @@ Feature: Intent Results Are Correctly Delivered And "libraryApp" is an app with the following intents | Intent Name | Context Type | Result Type | | returnBook | fdc3.book | {empty} | + And "App1/a1" is an app with the following intents + | Intent Name | Context Type | Result Type | + | viewNews | fdc3.instrument | {empty} | And A newly instantiated FDC3 Server And "LibraryApp/l1" is opened with connection id "l1" And "App1/a1" is opened with connection id "a1" And "LibraryApp/l1" registers an intent listener for "returnBook" + Scenario: Waiting for an intent listener to be Added + When "LibraryApp/l1" raises an intent for "viewNews" with contextType "fdc3.instrument" on app "App1/a1" with requestUuid "ABC123" + And "App1/a1" registers an intent listener for "viewNews" + And "App1/a1" sends a intentResultRequest with eventUuid "uuid10" and void contents and raiseIntentUuid "ABC123" + Then messaging will have outgoing posts + | msg.type | msg.meta.eventUuid | to.appId | to.instanceId | msg.payload.raiseIntentRequestUuid | msg.payload.intentResolution.source.instanceId | msg.payload.intentResult.context.type | + | intentEvent | uuid10 | App1 | a1 | ABC123 | {null} | {null} | + | raiseIntentResponse | {null} | LibraryApp | l1 | {null} | a1 | {null} | + | raiseIntentResultResponse | {null} | LibraryApp | l1 | {null} | {null} | {null} | + | intentResultResponse | {null} | App1 | a1 | {null} | {null} | {null} | + Scenario: App Returns An Intent Response ISSUE: 1303 prevents the use of matches_type When "App1/a1" raises an intent for "returnBook" with contextType "fdc3.book" on app "LibraryApp/l1" with requestUuid "ABC123" - When "LibraryApp/l1" sends a intentResultRequest with eventUuid "DEF123" and contextType "fdc3.book" and raiseIntentUuid "ABC123" + When "LibraryApp/l1" sends a intentResultRequest with eventUuid "uuid7" and contextType "fdc3.book" and raiseIntentUuid "ABC123" Then messaging will have outgoing posts - | msg.type | msg.meta.eventUuid | to.appId | to.instanceId | msg.payload.raiseIntentRequestUuid | msg.payload.intentResolution.source.instanceId | msg.payload.intentResult.context.type | - | intentEvent | uuid7 | LibraryApp | l1 | ABC123 | {null} | {null} | - | raiseIntentResponse | {null} | App1 | a1 | {null} | l1 | {null} | - | raiseIntentResultResponse | {null} | App1 | a1 | {null} | {null} | fdc3.book | - | intentResultResponse | {null} | LibraryApp | l1 | {null} | {null} | {null} | + | msg.type | msg.meta.eventUuid | msg.meta.requestUuid | to.appId | to.instanceId | msg.payload.raiseIntentRequestUuid | msg.payload.intentResolution.source.instanceId | msg.payload.intentResult.context.type | + | intentEvent | uuid7 | {null} | LibraryApp | l1 | ABC123 | {null} | {null} | + | raiseIntentResponse | {null} | ABC123 | App1 | a1 | {null} | l1 | {null} | + | raiseIntentResultResponse | {null} | uuid9 | App1 | a1 | {null} | {null} | fdc3.book | + | intentResultResponse | {null} | uuid9 | LibraryApp | l1 | {null} | {null} | {null} | Scenario: App Returns An Intent Result When "App1/a1" raises an intent for "returnBook" with contextType "fdc3.book" on app "LibraryApp/l1" with requestUuid "ABC123" - When "LibraryApp/l1" sends a intentResultRequest with eventUuid "ABC123" and private channel "pc1" + When "LibraryApp/l1" sends a intentResultRequest with eventUuid "uuid7" and private channel "pc1" and raiseIntentUuid "ABC123" Then messaging will have outgoing posts | msg.type | msg.meta.eventUuid | to.appId | to.instanceId | msg.payload.raiseIntentRequestUuid | msg.payload.intentResolution.source.instanceId | msg.payload.intentResult.channel.id | | intentEvent | uuid7 | LibraryApp | l1 | ABC123 | {null} | {null} | @@ -36,7 +50,7 @@ ISSUE: 1303 prevents the use of matches_type ISSUE: 1303 prevents the use of matches_type When "App1/a1" raises an intent for "returnBook" with contextType "fdc3.book" on app "LibraryApp/l1" with requestUuid "ABC123" - When "LibraryApp/l1" sends a intentResultRequest with eventUuid "ABC123" and void contents + When "LibraryApp/l1" sends a intentResultRequest with eventUuid "uuid7" and void contents and raiseIntentUuid "ABC123" Then messaging will have outgoing posts | msg.type | msg.meta.eventUuid | to.appId | to.instanceId | msg.payload.raiseIntentRequestUuid | msg.payload.intentResolution.source.instanceId | msg.payload.intentResult.context.type | | intentEvent | uuid7 | LibraryApp | l1 | ABC123 | {null} | {null} | diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/test/features/raise-intent.feature b/toolbox/fdc3-for-web/fdc3-web-impl/test/features/raise-intent.feature index 012c86aa0..f035d2928 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/test/features/raise-intent.feature +++ b/toolbox/fdc3-for-web/fdc3-web-impl/test/features/raise-intent.feature @@ -15,6 +15,11 @@ Feature: Raising Intents And "App1/a1" is opened with connection id "a1" And "listenerApp/b1" is opened with connection id "b1" And "listenerApp/b1" registers an intent listener for "returnBook" + # Scenario: Context Not Handled By App + # When "App1/a1" raises an intent for "borrowBook" with contextType "fdc3.magazine" on app "listenerApp/b1" + # Then messaging will have outgoing posts + # | msg.type | msg.payload.error | to.instanceId | + # | raiseIntentResponse | NoAppsFound | a1 | Scenario: Raising an Intent to a Non-Existent App And "App1/a1" raises an intent for "returnBook" with contextType "fdc3.book" on app "completelyMadeUp" @@ -28,12 +33,6 @@ Feature: Raising Intents | msg.type | msg.payload.error | to.instanceId | | raiseIntentResponse | TargetInstanceUnavailable | a1 | - Scenario: Context Not Handled By App - When "App1/a1" raises an intent for "borrowBook" with contextType "fdc3.bizboz" on app "listenerApp/b1" - Then messaging will have outgoing posts - | msg.type | msg.payload.error | to.instanceId | - | raiseIntentResponse | NoAppsFound | a1 | - Scenario: Raising An Intent To A Running App When "App1/a1" raises an intent for "returnBook" with contextType "fdc3.book" on app "listenerApp/b1" Then messaging will have outgoing posts @@ -106,20 +105,19 @@ Feature: Raising Intents | msg.payload.error | msg.type | | TargetInstanceUnavailable | raiseIntentResponse | - Scenario: Raising An Invalid Intent to the server (no app) + Scenario: Raising An Invalid Intent (no app) When "App1/a1" raises an intent for "borrowBook" with contextType "fdc3.book" on app "nonExistentApp" Then messaging will have outgoing posts | msg.payload.error | msg.type | | TargetAppUnavailable | raiseIntentResponse | - Scenario: Raising An Invalid Intent to the server (non existent intent) + Scenario: Raising An Invalid Intent (non existent intent) When "App1/a1" raises an intent for "nonExistentIntent" with contextType "fdc3.book" Then messaging will have outgoing posts | msg.payload.error | msg.type | | NoAppsFound | raiseIntentResponse | - - Scenario: Raising An Invalid Intent to the server - When "App1/a1" raises an intent for "nonExistentIntent" with contextType "fdc3.book" on app "listenerApp/b1" - Then messaging will have outgoing posts - | msg.payload.error | msg.type | - | NoAppsFound | raiseIntentResponse | + # Scenario: Raising An Invalid Intent (non existent intent but valid app) + # When "App1/a1" raises an intent for "nonExistentIntent" with contextType "fdc3.book" on app "listenerApp/b1" + # Then messaging will have outgoing posts + # | msg.payload.error | msg.type | + # | NoAppsFound | raiseIntentResponse | diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/generic.steps.ts b/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/generic.steps.ts index 862a65c9e..ce369fdad 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/generic.steps.ts +++ b/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/generic.steps.ts @@ -1,4 +1,4 @@ -import { Given } from '@cucumber/cucumber' +import { Given, When } from '@cucumber/cucumber' import { CustomWorld } from '../world'; import { TestServerContext } from '../support/TestServerContext'; import { DefaultFDC3Server } from '../../src/BasicFDC3Server'; @@ -45,25 +45,8 @@ export const contextMap: Record = { } } - -export function createMeta(cw: CustomWorld, appStr: string) { - const [appId, instanceId] = appStr.split("/") - const app = { appId, instanceId } - - return { - "requestUuid": cw.sc.createUUID(), - "timestamp": new Date(), - "source": app - } -} - -Given('A newly instantiated FDC3 Server', function (this: CustomWorld) { - const apps = this.props[APP_FIELD] ?? [] - const d = new BasicDirectory(apps) - - - this.sc = new TestServerContext(this) - this.server = new DefaultFDC3Server(this.sc, d, [ +function defaultChannels() { + return [ { id: 'one', type: ChannelType.user, @@ -91,6 +74,38 @@ Given('A newly instantiated FDC3 Server', function (this: CustomWorld) { color: 'ochre' } } - ], 2000, 2000) + ] +} + +export function createMeta(cw: CustomWorld, appStr: string) { + const [appId, instanceId] = appStr.split("/") + const app = { appId, instanceId } + + return { + "requestUuid": cw.sc.createUUID(), + "timestamp": new Date(), + "source": app + } +} + +Given('A newly instantiated FDC3 Server', function (this: CustomWorld) { + const apps = this.props[APP_FIELD] ?? [] + const d = new BasicDirectory(apps) + + + this.sc = new TestServerContext(this) + this.server = new DefaultFDC3Server(this.sc, d, defaultChannels(), false, 2000, 2000) +}); + +Given('A newly instantiated FDC3 Server with heartbeat checking', function (this: CustomWorld) { + const apps = this.props[APP_FIELD] ?? [] + const d = new BasicDirectory(apps) + + + this.sc = new TestServerContext(this) + this.server = new DefaultFDC3Server(this.sc, d, defaultChannels(), true, 2000, 2000) +}); -}); \ No newline at end of file +When("I shutdown the server", function (this: CustomWorld) { + this.server.shutdown() +}) diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/heartbeat.steps.ts b/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/heartbeat.steps.ts new file mode 100644 index 000000000..68feed8c1 --- /dev/null +++ b/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/heartbeat.steps.ts @@ -0,0 +1,24 @@ +import { Given, Then } from "@cucumber/cucumber"; +import { CustomWorld } from "../world"; +import { HeartbeatAcknowledgementRequest } from "@kite9/fdc3-schema/generated/api/BrowserTypes"; +import { createMeta } from "./generic.steps"; + +Given('{string} sends a heartbeat response', function (this: CustomWorld, appStr: string) { + const meta = createMeta(this, appStr) + const uuid = this.sc.getInstanceUUID(meta.source)!! + + const message = { + meta, + payload: { + timestamp: new Date() + }, + type: 'heartbeatAcknowledgementRequest' + } as HeartbeatAcknowledgementRequest + + this.server.receive(message, uuid) +}); + +Then('I test the liveness of {string}', async function (this: CustomWorld, appStr: string) { + const out = await this.sc.isAppConnected(createMeta(this, appStr).source) + this.props["result"] = out +}) \ No newline at end of file diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/intents.steps.ts b/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/intents.steps.ts index ef672a60c..189e74b20 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/intents.steps.ts +++ b/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/intents.steps.ts @@ -4,7 +4,6 @@ import { DirectoryApp } from "../../src/directory/DirectoryInterface"; import { APP_FIELD, contextMap, createMeta } from "./generic.steps"; import { handleResolve } from "@kite9/testing"; import { BrowserTypes } from '@kite9/fdc3-schema'; -import { v4 as uuid } from 'uuid' type FindIntentRequest = BrowserTypes.FindIntentRequest type FindIntentsByContextRequest = BrowserTypes.FindIntentsByContextRequest @@ -221,8 +220,7 @@ When('{string} sends a intentResultRequest with eventUuid {string} and contextTy const message: IntentResultRequest = { type: 'intentResultRequest', meta: { - requestUuid: uuid(), - timestamp: new Date() + ...meta }, payload: { intentResult: { @@ -236,34 +234,32 @@ When('{string} sends a intentResultRequest with eventUuid {string} and contextTy }) -When('{string} sends a intentResultRequest with eventUuid {string} and void contents', function (this: CustomWorld, appStr: string, requestUuid: string) { +When('{string} sends a intentResultRequest with eventUuid {string} and void contents and raiseIntentUuid {string}', function (this: CustomWorld, appStr: string, eventUuid: string, raiseIntentUuid: string) { const meta = createMeta(this, appStr) const uuid = this.sc.getInstanceUUID(meta.source)!! const message: IntentResultRequest = { type: 'intentResultRequest', meta: { - requestUuid: meta.requestUuid, - timestamp: new Date() + ...meta }, payload: { intentResult: { }, - intentEventUuid: 'event-uuid-1', - raiseIntentRequestUuid: requestUuid + intentEventUuid: eventUuid, + raiseIntentRequestUuid: raiseIntentUuid } } this.server.receive(message, uuid) }) -When('{string} sends a intentResultRequest with eventUuid {string} and private channel {string}', function (this: CustomWorld, appStr: string, requestUuid: string, channelId: string) { +When('{string} sends a intentResultRequest with eventUuid {string} and private channel {string} and raiseIntentUuid {string}', function (this: CustomWorld, appStr: string, eventUuid: string, channelId: string, raiseIntentUuid: string) { const meta = createMeta(this, appStr) const uuid = this.sc.getInstanceUUID(meta.source)!! const message: IntentResultRequest = { type: 'intentResultRequest', meta: { - requestUuid: meta.requestUuid, - timestamp: new Date() + ...meta }, payload: { intentResult: { @@ -272,34 +268,34 @@ When('{string} sends a intentResultRequest with eventUuid {string} and private c id: channelId } }, - intentEventUuid: 'event-uuid-1', - raiseIntentRequestUuid: requestUuid + intentEventUuid: eventUuid, + raiseIntentRequestUuid: raiseIntentUuid } } this.server.receive(message, uuid) }) -When('{string} sends a intentResultRequest with eventUuid {string}', function (this: CustomWorld, appStr: string, requestUuid: string) { - const meta = createMeta(this, appStr) - const uuid = this.sc.getInstanceUUID(meta.source)!! - const message = { - type: 'intentResultRequest', - meta: { - requestUuid: meta.requestUuid, - responseUuid: this.sc.createUUID(), - timestamp: new Date() - }, - payload: { - intentResult: { - context: { - "type": "fdc3.something", - "name": "Some Name" - } - }, - intentEventUuid: 'event-uuid-1', - raiseIntentRequestUuid: requestUuid - } - } as IntentResultRequest - - this.server.receive(message, uuid) -}); \ No newline at end of file +// When('{string} sends a intentResultRequest with eventUuid {string}', function (this: CustomWorld, appStr: string, requestUuid: string) { +// const meta = createMeta(this, appStr) +// const uuid = this.sc.getInstanceUUID(meta.source)!! +// const message = { +// type: 'intentResultRequest', +// meta: { +// requestUuid: meta.requestUuid, +// responseUuid: this.sc.createUUID(), +// timestamp: new Date() +// }, +// payload: { +// intentResult: { +// context: { +// "type": "fdc3.something", +// "name": "Some Name" +// } +// }, +// intentEventUuid: 'event-uuid-1', +// raiseIntentRequestUuid: requestUuid +// } +// } as IntentResultRequest + +// this.server.receive(message, uuid) +// }); \ No newline at end of file diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/start-app.steps.ts b/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/start-app.steps.ts index ccdafc2f5..3071ab5ec 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/start-app.steps.ts +++ b/toolbox/fdc3-for-web/fdc3-web-impl/test/step-definitions/start-app.steps.ts @@ -33,7 +33,9 @@ When('{string} sends validate', function (this: CustomWorld, uuid: string) { timestamp: new Date() }, payload: { - } as any /* ISSUE: 1301 */ + actualUrl: "something", + identityUrl: "something" + } } this.sc.setAppConnected(identity!!) this.server.receive(message, uuid) diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/test/support/TestServerContext.ts b/toolbox/fdc3-for-web/fdc3-web-impl/test/support/TestServerContext.ts index 3e0136255..dd0f396e0 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/test/support/TestServerContext.ts +++ b/toolbox/fdc3-for-web/fdc3-web-impl/test/support/TestServerContext.ts @@ -1,6 +1,7 @@ import { ServerContext, InstanceID } from '../../src/ServerContext' import { CustomWorld } from '../world' -import { OpenError, AppIdentifier, AppIntent, Context } from '@kite9/fdc3' +import { Context } from '@kite9/fdc3-context' +import { OpenError, AppIdentifier, AppIntent } from '@kite9/fdc3-standard' type ConnectionDetails = AppIdentifier & { msg?: object @@ -25,6 +26,7 @@ export class TestServerContext implements ServerContext { this.cw = cw } + async narrowIntents(appIntents: AppIntent[], _context: Context): Promise { return appIntents } @@ -60,6 +62,13 @@ export class TestServerContext implements ServerContext { this.instances.find(ca => (ca.instanceId == app.instanceId))!!.connected = true } + async setAppDisconnected(app: AppIdentifier): Promise { + const idx = this.instances.findIndex(ca => (ca.instanceId == app.instanceId)) + if (idx != -1) { + this.instances.splice(idx, 1) + } + } + async getConnectedApps(): Promise { return this.instances.filter(ca => ca.connected).map(x => { return { diff --git a/toolbox/fdc3-for-web/fdc3-web-impl/test/world/index.ts b/toolbox/fdc3-for-web/fdc3-web-impl/test/world/index.ts index 2f9f43d50..2eaa83820 100644 --- a/toolbox/fdc3-for-web/fdc3-web-impl/test/world/index.ts +++ b/toolbox/fdc3-for-web/fdc3-web-impl/test/world/index.ts @@ -6,7 +6,7 @@ import { BasicDirectory } from "../../src/directory/BasicDirectory"; export class CustomWorld extends World { sc = new TestServerContext(this) - server = new DefaultFDC3Server(this.sc, new BasicDirectory([]), []) + server = new DefaultFDC3Server(this.sc, new BasicDirectory([]), [], false) props: Record = {} } diff --git a/toolbox/fdc3-workbench/package-lock.json b/toolbox/fdc3-workbench/package-lock.json index f9c483d5f..6882239db 100644 --- a/toolbox/fdc3-workbench/package-lock.json +++ b/toolbox/fdc3-workbench/package-lock.json @@ -1,12 +1,12 @@ { "name": "fdc3-workbench", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fdc3-workbench", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "license": "Apache-2.0", "devDependencies": { "@apidevtools/json-schema-ref-parser": "^9.0.9", diff --git a/toolbox/fdc3-workbench/package.json b/toolbox/fdc3-workbench/package.json index acbde7e5e..1c7b30dd1 100644 --- a/toolbox/fdc3-workbench/package.json +++ b/toolbox/fdc3-workbench/package.json @@ -1,6 +1,6 @@ { "name": "fdc3-workbench", - "version": "2.2.0-beta.16", + "version": "2.2.0-beta.20", "private": true, "homepage": ".", "license": "Apache-2.0", @@ -30,7 +30,7 @@ ] }, "devDependencies": { - "@kite9/fdc3": "2.2.0-beta.16", + "@kite9/fdc3": "2.2.0-beta.20", "@types/jsoneditor": "^8.6.1", "@typescript-eslint/eslint-plugin": "7.1.1", "@typescript-eslint/parser": "7.1.0", @@ -75,4 +75,4 @@ "*.js": "eslint --cache --fix", "*.{js,css,md}": "prettier --write" } -} \ No newline at end of file +} diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index bbaf5d1c8..6cc8dca20 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -210,6 +210,27 @@ module.exports={ "width": 50, "href": "https://finos.org" } + }, + "mermaid": { + "options": { + "htmlLabels": true, + "markdownAutoWrap": true, + "wrap": true, + "wrappingWidth": 50, + "flowchart": { + "titleTopMargin": 30, + "subGraphTitleMargin": { + "top": 30, + "bottom": 30 + }, + "nodeSpacing": 30, + "rankSpacing": 50, + "diagramPadding": 5, + "useMaxWidth": true, + "htmlLabels": true, + "wrappingWidth": 50 + } + } } } }