Skip to content

Commit

Permalink
Merge pull request #14 from finos-labs/robs-branch
Browse files Browse the repository at this point in the history
First intent tests working
  • Loading branch information
robmoffat authored Mar 21, 2024
2 parents 11df194 + 90f7738 commit 8d26526
Show file tree
Hide file tree
Showing 174 changed files with 14,468 additions and 2,013 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ packages/da/tsconfig.tsbuildinfo
packages/testing/tsconfig.tsbuildinfo
**/coverage
.nyc_output
packages/da-server/generated
12 changes: 8 additions & 4 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
packageExtensions:
"@cucumber/cucumber@*":
dependencies:
"ts-node": "*"
"@cucumber/pretty-formatter": "*"

yarnPath: .yarn/releases/yarn-4.0.2.cjs
"@cucumber/pretty-formatter": "*"
ts-node: "*"
vite-express@*:
dependencies:
express: "*"
vite: "*"

yarnPath: .yarn/releases/yarn-4.1.1.cjs
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ This is a minimal proof-of-concept for FDC3 For the Web.

## To Run

0. Prerequisites:

```
node: v20+
yarn v4+
```
1. From the Command Line:
```
yarn install
Expand Down Expand Up @@ -35,7 +42,9 @@ The project is divided into several different yarn workspaces:
- `common` : Common APIs and functionality used by both `client` and `server`
- `demo` : A bare-bones desktop agent implementation with a few apps that use WebFDC3
- `fdc3-web-demo` : A bare-bones desktop agent implementation with a few apps that use WebFDC3. See: https://static.swimlanes.io/6bb69f2c9acdc0656f5f3b098d40518e.png for how this works. Basically, the implementation here is that it uses iframes approach and a server-side websocket to relay messages.
- `fdc3-workbench`: The FDC3 Workbench app from https://github.com/FDC3/toolbox/workbench, ported to use WebFDC3.
## Configuring the client
Expand Down Expand Up @@ -82,3 +91,19 @@ Desktop Agent Briding needs extending with the following types:
## Troubleshooting
- Try removing tsconfig.tsbuildinfo files if you are having trouble building
## Issues To Resolve
- How does the da-server tell the da-proxy about the channel metadata? We need a message to get the list of user channels from the server.
- How does the da-server decide on a desktop agent name (maybe it just has one?)
- AppChecker / AppDetailsResolver / AppPortResolver - this is all too complex.
= fdc3Ready timeout
- get it to work without desktop agent window running
- use cookie for the da id.
- add server tests for intent resolution choice
- handle disconnections from the server / update running apps
## Idea
Do we need to send a post-message to the server, if we have cookies? Couldn't we just hold the DA ID and the
address of the embed page in the cookie? Problem is, the cookie is scoped to the DA...
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"scripts": {
"build": "yarn workspaces foreach --all run build"
},
"packageManager": "yarn@4.0.2",
"packageManager": "yarn@4.1.1",
"devDependencies": {
"typescript": "^5.3.2"
}
Expand Down
1 change: 1 addition & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "client",
"main": "src/index.ts",
"version": "0.0.0",
"scripts": {
"build": "tsc -b"
},
Expand Down
19 changes: 14 additions & 5 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { DesktopAgent} from '@finos/fdc3'
import { DesktopAgent } from '@finos/fdc3'
import { Options } from 'fdc3-common';
import postMessage from './strategies/post-message'
//import electronEvent from './strategies/electron-event'

export const DEFAULT_OPTIONS : Options = {
export const DEFAULT_OPTIONS: Options = {
setWindowGlobal: false,
fireFdc3Ready: false,
strategies: [postMessage], //, electronEvent],
Expand All @@ -13,7 +13,7 @@ export const DEFAULT_OPTIONS : Options = {
/**
* This return an FDC3 API. Called by Apps.
*/
export function getClientAPI(optionsOverride: Options = DEFAULT_OPTIONS) : Promise<DesktopAgent> {
export function getClientAPI(optionsOverride: Options = DEFAULT_OPTIONS): Promise<DesktopAgent> {

const options = {
...DEFAULT_OPTIONS,
Expand All @@ -24,12 +24,21 @@ export function getClientAPI(optionsOverride: Options = DEFAULT_OPTIONS) : Promi
if ((options.setWindowGlobal) && (window.fdc3 == null)) {
window.fdc3 = da;
}

return da;
}

const strategies = options.strategies!!.map(s => s(options));

return Promise.any(strategies)
.then(da => handleGenericOptions(da))
.then(da => handleGenericOptions(da))
}

export function fdc3Ready(waitForMs?: number): Promise<DesktopAgent> {
return getClientAPI({
...DEFAULT_OPTIONS,
waitForMs,
setWindowGlobal: true,
fireFdc3Ready: true
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { AppIntent, IntentResult } from "@finos/fdc3";
import { Messaging } from "da-proxy";
import { IntentResolver, SingleAppIntent, IntentResolutionChoiceAgentResponse } from "fdc3-common";

/**
* Works with the desktop agent to provide a resolution to the intent choices.
*/
export class DesktopAgentIntentResolver implements IntentResolver {

private readonly m: Messaging
private readonly uri: string
private container: HTMLDivElement | undefined = undefined

constructor(m: Messaging, uri: string) {
this.m = m
this.uri = uri
}

async intentChosen(ir: IntentResult): Promise<IntentResult> {
this.removeFrame()
return ir
}

async chooseIntent(appIntents: AppIntent[]): Promise<SingleAppIntent> {
this.openFrame(appIntents)

const choice = await this.m.waitFor<IntentResolutionChoiceAgentResponse>(m => m.type == 'intentResolutionChoice')

this.removeFrame()

return choice.payload
}

removeFrame() {
if (this.container) {
document.body.removeChild(this.container)
this.container = undefined
}
}

buildUrl(appIntents: AppIntent[]): string {
return this.uri + "?intentDetails=" + encodeURIComponent(JSON.stringify(appIntents)) +
"&source=" + encodeURIComponent(JSON.stringify(this.m.getSource()))
}

openFrame(appIntents: AppIntent[]): void {
this.removeFrame()

this.container = document.createElement("div")
document.body.appendChild(this.container)
this.container.style.position = "fixed"
this.container.style.zIndex = "1000"
this.container.style.left = "10%"
this.container.style.top = "10%"
this.container.style.right = "10%"
this.container.style.bottom = "10%"

var ifrm = document.createElement("iframe")
ifrm.setAttribute("src", this.buildUrl(appIntents))
ifrm.setAttribute("name", "FDC3 Communications")
ifrm.style.width = "100%"
ifrm.style.height = "100%"
this.container.appendChild(ifrm)
}
}
26 changes: 8 additions & 18 deletions packages/client/src/messaging/MessagePortMessaging.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
import { AppIdentifier } from "@finos/fdc3"
import { AgentRequestMessage } from "@finos/fdc3/dist/bridging/BridgingTypes"
import { Messaging } from "da-proxy"
import { AbstractMessaging } from "da-proxy"
import { RegisterableListener } from "da-proxy/src/listeners/RegisterableListener"
import { exchangePostMessage } from "fdc3-common"
import { v4 as uuidv4 } from "uuid"

type ListenerDetail = {
filter: (m: AgentRequestMessage) => boolean,
action: (m: AgentRequestMessage) => void
}

export class MessagePortMessaging implements Messaging {
export class MessagePortMessaging extends AbstractMessaging {

private readonly appId: AppIdentifier
private readonly mp: MessagePort
private readonly listeners : Map<string, ListenerDetail> = new Map()
private readonly listeners: Map<string, RegisterableListener> = new Map()

constructor(mp: MessagePort, appId: AppIdentifier) {
super()
this.appId = appId
this.mp = mp;

this.mp.onmessage = (m) => {
this.listeners.forEach((v, _k) => {
if (v.filter(m.data)) {
v.action(m.data)
}
}
})
}
}
Expand All @@ -40,14 +36,8 @@ export class MessagePortMessaging implements Messaging {
return Promise.resolve();
}

register(filter: (m: any) => boolean, action: (m: any) => void): string {
const id = this.createUUID();
this.listeners.set(id,{
filter,
action
})

return id;
register(l: RegisterableListener): void {
this.listeners.set(l.id, l)
}

unregister(id: string): void {
Expand Down
89 changes: 36 additions & 53 deletions packages/client/src/messaging/message-port.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,61 @@
import { Context, DesktopAgent } from "@finos/fdc3";
import { BasicDesktopAgent, DefaultChannelSupport, DefaultAppSupport, DefaultIntentSupport, DefaultChannel } from "da";
import { APIResponseMessage, FDC3_PORT_TRANSFER_RESPONSE_TYPE, FDC3_PORT_TRANSFER_REQUEST_TYPE, Options, exchangeForMessagePort, exchange } from "fdc3-common"
import { DesktopAgent } from "@finos/fdc3";
import { BasicDesktopAgent, DefaultChannelSupport, DefaultAppSupport, DefaultIntentSupport, DefaultChannel, DefaultHandshakeSupport } from "da-proxy";
import { APIResponseMessage, FDC3_PORT_TRANSFER_RESPONSE_TYPE, FDC3_PORT_TRANSFER_REQUEST_TYPE, Options, exchangeForMessagePort, APIResponseMessageParentWindow, APIResponseMessageIFrame } from "fdc3-common"
import { MessagePortMessaging } from "./MessagePortMessaging";
import { ConnectionStep2Hello, ConnectionStep3Handshake } from "@finos/fdc3/dist/bridging/BridgingTypes";
import { DesktopAgentIntentResolver } from "../intent-resolution/DesktopAgentIntentResolver";

/**
* Given a message port, constructs a desktop agent to communicate via that.
*/
export async function messagePortInit(mp: MessagePort, data: APIResponseMessage) : Promise<DesktopAgent> {
export async function createDesktopAgentAPI(mp: MessagePort, data: APIResponseMessage, options: Options): Promise<DesktopAgent> {
mp.start()

const handshakeData = (await exchange(mp, "handshake", () => sendHello(mp, data))).data as ConnectionStep3Handshake
const messaging = new MessagePortMessaging(mp, data.appIdentifier)
const channelState = handshakeData.payload.channelsState
const userChannelState = buildUserChannelState(messaging, channelState)

return new BasicDesktopAgent(
new DefaultChannelSupport(messaging, userChannelState, null),
new DefaultIntentSupport(),
new DefaultAppSupport(messaging, data.appIdentifier),
data.fdc3Version,
data.provider);
const intentResolver = options.intentResolver ?? new DesktopAgentIntentResolver(messaging, data.resolverUri)
const userChannelState = buildUserChannelState(messaging)

const version = "2.0"
const cs = new DefaultChannelSupport(messaging, userChannelState, null)
const hs = new DefaultHandshakeSupport(messaging, [version], cs)
const is = new DefaultIntentSupport(messaging, intentResolver)
const as = new DefaultAppSupport(messaging, data.appIdentifier, "WebFDC3")
const da = new BasicDesktopAgent(hs, cs, is, as, version)
await da.connect()
return da
}

/**
* Initialises the desktop agent by opening an iframe
* Initialises the desktop agent by opening an iframe or talking to the parent window.
* on the desktop agent host and communicating via a messsage port to it.
*
* It is up to the desktop agent to arrange communucation between other
* windows.
*/
export async function messagePortIFrameInit(data: APIResponseMessage, options: Options) : Promise<DesktopAgent> {

const action = data.uri ? () => {
return openFrame(data.uri!!);
} : () => {
return messageParentWindow(options.frame)
}
export async function messagePortInit(event: MessageEvent, options: Options): Promise<DesktopAgent> {

const mp = await exchangeForMessagePort(window, FDC3_PORT_TRANSFER_RESPONSE_TYPE, action) as MessagePort

return messagePortInit(mp, data);
}
if (event.ports[0]) {
return createDesktopAgentAPI(event.ports[0], event.data, options);
} else if ((event.data as APIResponseMessageIFrame).uri) {
const action = () => {
const iframeData = event.data as APIResponseMessageIFrame
return openFrame(iframeData.uri +
"?source=" + encodeURIComponent(JSON.stringify(iframeData.appIdentifier)) +
"&desktopAgentId=" + encodeURIComponent(iframeData.desktopAgentId));
}

/**
* If the desktop agent doesn't provide an opener URL, we message another iframe asking for the port.
*/
function messageParentWindow(w: Window | undefined) {
if (w) {
w.postMessage({
type: FDC3_PORT_TRANSFER_REQUEST_TYPE,
methods: 'post-message'
});
const mp = await exchangeForMessagePort(window, FDC3_PORT_TRANSFER_RESPONSE_TYPE, action) as MessagePort
return createDesktopAgentAPI(mp, event.data, options);

} else {
throw new Error(`Couldn't initialise message port with ${JSON.stringify(event)}`)
}
}

/**
* The desktop agent requests that the client opens a URL in order to provide a message port.
*/
function openFrame(url: string) : Window {
function openFrame(url: string): Window {
var ifrm = document.createElement("iframe")
ifrm.setAttribute("src", url)
ifrm.setAttribute("name", "FDC3 Communications")
Expand All @@ -68,23 +65,9 @@ function openFrame(url: string) : Window {
return ifrm.contentWindow!!
}

function sendHello(mp: MessagePort, data: APIResponseMessage) {
const hello : ConnectionStep2Hello = {
type: "hello",
payload: {
desktopAgentBridgeVersion: data.desktopAgentBridgeVersion,
supportedFDC3Versions: data.supportedFDC3Versions,
authRequired: data.authRequired,
authToken: data.authToken
},
meta: {
timestamp: new Date()
}
}
mp.postMessage(hello);
}

function buildUserChannelState(messaging: MessagePortMessaging, _channelState: Record<string, Context[]>) {
function buildUserChannelState(messaging: MessagePortMessaging) {
// TODO: Figure out how to set initial user channels.
// Should probably be in the message from the server.
return [
new DefaultChannel(messaging, "one", "user", {
color: "red",
Expand Down
Loading

0 comments on commit 8d26526

Please sign in to comment.