Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fdc3Ready helper function and getInfo to npm package exports #360

Merged
merged 3 commits into from
Apr 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
# ignore folders
website
dist
src/app-directory/*/target
20 changes: 11 additions & 9 deletions docs/api/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,26 @@ The [`@finos/fdc3` npm package](https://www.npmjs.com/package/@finos/fdc3) provi
```ts
import * as fdc3 from '@finos/fdc3'

const listener = fdc3.addIntentListener('ViewAnalysis', context => {
// do something
await fdc3.raiseIntent('ViewAnalysis', {
type: 'fdc3.instrument',
id: { ticker: 'AAPL' }
})
```

Alternatively you can also import individual operations directly:
It also includes a helper function you can use to wait for FDC3 to become available:

```ts
import { raiseIntent } from '@finos/fdc3'
import { fdc3Ready, addIntentListener } from '@finos/fdc3'

await raiseIntent('ViewAnalysis', {
type: 'fdc3.instrument',
id: { ticker: 'AAPL' }
await fdc3Ready();

const listener = addIntentListener('ViewAnalysis', instrument => {
// handle intent
})
```

The npm package will take care of checking for the existence of the global `fdc3` object, and wait for the `fdc3Ready` event, or throw an error if FDC3 is not supported.

#### See also
* [`fdc3Ready() Function`](ref/Globals#fdc3ready-function)



29 changes: 28 additions & 1 deletion docs/api/ref/Globals.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,33 @@ function fdc3Action() {
if (window.fdc3) {
fdc3Action();
} else {
window.addEventListener("fdc3Ready", fdc3Action);
window.addEventListener('fdc3Ready', fdc3Action);
}
```

## `fdc3Ready()` Function

If you are using the `@finos/fdc3` NPM package, it includes a handy wrapper function that will check for the existence of `window.fdc3` and wait on the `fdc3Ready` event for you.

It returns a promise that will resolve immediately if the `window.fdc3` global is already defined, or reject with an error if the `fdc3Ready` event doesn't fire after a specified timeout period (default: 5 seconds).

### Example

```ts
import { fdc3Ready, broadcast } from '@finos/fdc3'

async function fdc3Action() {
try {
await fdc3Ready(1000); // wait for (at most) 1 second
broadcast({
type: 'fdc3.instrument',
id: { ticker: 'AAPL' }
})
} catch (error) {
// handle error
}
}
```



4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"singleQuote": true,
"arrowParens": "avoid",
"trailingComma": "es5",
"endOfLine": "auto"
"endOfLine": "auto",
"printWidth": 120
},
"resolutions": {
"node-fetch": "^2.6.1",
Expand All @@ -47,6 +48,7 @@
},
"devDependencies": {
"husky": "^4.3.0",
"jest-mock-extended": "^1.0.13",
"quicktype": "^15.0.258",
"tsdx": "^0.14.1",
"tslib": "^2.0.1",
Expand Down
5 changes: 1 addition & 4 deletions src/api/Channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,5 @@ export interface Channel {
/**
* Adds a listener for incoming contexts of the specified context type whenever a broadcast happens on this channel.
*/
addContextListener(
contextType: string | null,
handler: ContextHandler
): Listener;
addContextListener(contextType: string | null, handler: ContextHandler): Listener;
}
16 changes: 3 additions & 13 deletions src/api/DesktopAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,22 +124,15 @@ export interface DesktopAgent {
* await fdc3.raiseIntent("StartChat", context, appMetadata);
* ```
*/
raiseIntent(
intent: string,
context: Context,
app?: TargetApp
): Promise<IntentResolution>;
raiseIntent(intent: string, context: Context, app?: TargetApp): Promise<IntentResolution>;

/**
* Raises a context to the desktop agent to resolve with one of the possible Intents for that context.
* ```javascript
* await fdc3.raiseIntentForContext(context);
* ```
*/
raiseIntentForContext(
context: Context,
app?: TargetApp
): Promise<IntentResolution>;
raiseIntentForContext(context: Context, app?: TargetApp): Promise<IntentResolution>;

/**
* Adds a listener for incoming Intents from the Agent.
Expand All @@ -155,10 +148,7 @@ export interface DesktopAgent {
/**
* Adds a listener for the broadcast of a specific type of context object.
*/
addContextListener(
contextType: string | null,
handler: ContextHandler
): Listener;
addContextListener(contextType: string | null, handler: ContextHandler): Listener;

/**
* Retrieves a list of the System channels available for the app to join
Expand Down
161 changes: 73 additions & 88 deletions src/api/Methods.ts
Original file line number Diff line number Diff line change
@@ -1,113 +1,105 @@
import {
AppIntent,
Channel,
Context,
ContextHandler,
IntentResolution,
Listener,
ImplementationMetadata,
} from '..';
import { AppIntent, Channel, Context, ContextHandler, IntentResolution, Listener, ImplementationMetadata } from '..';
import { TargetApp } from './Types';

const unavailableError = new Error(
'FDC3 DesktopAgent not available at `window.fdc3`.'
);
const DEFAULT_TIMEOUT = 5000;

const rejectIfNoGlobal = (f: () => Promise<any>) => {
return window.fdc3 ? f() : Promise.reject(unavailableError);
};
const UnavailableError = new Error('FDC3 DesktopAgent not available at `window.fdc3`.');
const TimeoutError = new Error('Timed out waiting for `fdc3Ready` event.');
const UnexpectedError = new Error('`fdc3Ready` event fired, but `window.fdc3` not set to DesktopAgent.');

function rejectIfNoGlobal(f: () => Promise<any>) {
return window.fdc3 ? f() : Promise.reject(UnavailableError);
}

const throwIfNoGlobal = (f: () => any) => {
function throwIfNoGlobal(f: () => any) {
if (!window.fdc3) {
throw unavailableError;
throw UnavailableError;
}
return f();
}

export const fdc3Ready = async (waitForMs = DEFAULT_TIMEOUT): Promise<void> => {
return new Promise((resolve, reject) => {
// if the global is already available resolve immediately
if (window.fdc3) {
resolve();
} else {
// if its not available setup a timeout to return a rejected promise
const timeout = setTimeout(() => (window.fdc3 ? resolve() : reject(TimeoutError)), waitForMs);
// listen for the fdc3Ready event
window.addEventListener(
'fdc3Ready',
() => {
clearTimeout(timeout);
window.fdc3 ? resolve() : reject(UnexpectedError);
},
{ once: true }
);
}
});
};

export const open: (app: TargetApp, context?: Context) => Promise<void> = (
app,
context
) => {
export function open(app: TargetApp, context?: Context): Promise<void> {
return rejectIfNoGlobal(() => window.fdc3.open(app, context));
};
}

export const findIntent: (
intent: string,
context?: Context
) => Promise<AppIntent> = (intent, context) => {
export function findIntent(intent: string, context?: Context): Promise<AppIntent> {
return rejectIfNoGlobal(() => window.fdc3.findIntent(intent, context));
};
}

export const findIntentsByContext: (
context: Context
) => Promise<Array<AppIntent>> = context => {
export function findIntentsByContext(context: Context): Promise<AppIntent[]> {
return rejectIfNoGlobal(() => window.fdc3.findIntentsByContext(context));
};
}

export const broadcast: (context: Context) => void = context => {
export function broadcast(context: Context): void {
throwIfNoGlobal(() => window.fdc3.broadcast(context));
};
}

export const raiseIntent: (
intent: string,
context: Context,
app?: TargetApp
) => Promise<IntentResolution> = (intent, context, app) => {
export function raiseIntent(intent: string, context: Context, app?: TargetApp): Promise<IntentResolution> {
return rejectIfNoGlobal(() => window.fdc3.raiseIntent(intent, context, app));
};
}

export const raiseIntentForContext: (
context: Context,
app?: TargetApp
) => Promise<IntentResolution> = (context, app) => {
return rejectIfNoGlobal(() =>
window.fdc3.raiseIntentForContext(context, app)
);
};
export function raiseIntentForContext(context: Context, app?: TargetApp): Promise<IntentResolution> {
return rejectIfNoGlobal(() => window.fdc3.raiseIntentForContext(context, app));
}

export const addIntentListener: (
intent: string,
handler: ContextHandler
) => Listener = (intent, handler) => {
export function addIntentListener(intent: string, handler: ContextHandler): Listener {
return throwIfNoGlobal(() => window.fdc3.addIntentListener(intent, handler));
};
}

export const addContextListener: (
contextTypeOrHandler: string | ContextHandler,
handler?: ContextHandler
) => Listener = (a, b) => {
if (typeof a !== 'function') {
export function addContextListener(contextTypeOrHandler: string | ContextHandler, handler?: ContextHandler): Listener {
if (typeof contextTypeOrHandler !== 'function') {
return throwIfNoGlobal(() =>
window.fdc3.addContextListener(a as string, b as ContextHandler)
window.fdc3.addContextListener(contextTypeOrHandler as string, handler as ContextHandler)
);
} else {
return throwIfNoGlobal(() =>
window.fdc3.addContextListener(a as ContextHandler)
);
return throwIfNoGlobal(() => window.fdc3.addContextListener(contextTypeOrHandler as ContextHandler));
}
};
}

export const getSystemChannels: () => Promise<Array<Channel>> = () => {
export function getSystemChannels(): Promise<Channel[]> {
return rejectIfNoGlobal(() => window.fdc3.getSystemChannels());
};
}

export const joinChannel: (channelId: string) => Promise<void> = channelId => {
export function joinChannel(channelId: string): Promise<void> {
return rejectIfNoGlobal(() => window.fdc3.joinChannel(channelId));
};
}

export const getOrCreateChannel: (
channelId: string
) => Promise<Channel> = channelId => {
export function getOrCreateChannel(channelId: string): Promise<Channel> {
return rejectIfNoGlobal(() => window.fdc3.getOrCreateChannel(channelId));
};
}

export const getCurrentChannel: () => Promise<Channel | null> = () => {
export function getCurrentChannel(): Promise<Channel | null> {
return rejectIfNoGlobal(() => window.fdc3.getCurrentChannel());
};
}

export const leaveCurrentChannel: () => Promise<void> = () => {
export function leaveCurrentChannel(): Promise<void> {
return rejectIfNoGlobal(() => window.fdc3.leaveCurrentChannel());
};
}

export function getInfo(): ImplementationMetadata {
return throwIfNoGlobal(() => window.fdc3.getInfo());
}

/**
* Compare numeric semver version number strings (in the form `1.2.3`).
Expand All @@ -119,19 +111,12 @@ export const leaveCurrentChannel: () => Promise<void> = () => {
* @param a
* @param b
*/
export const compareVersionNumbers: (a: string, b: string) => number | null = (
a,
b
) => {
export const compareVersionNumbers: (a: string, b: string) => number | null = (a, b) => {
try {
let aVerArr = a.split('.').map(Number);
let bVerArr = b.split('.').map(Number);
for (
let index = 0;
index < Math.max(aVerArr.length, bVerArr.length);
index++
) {
/* If one version number has more digits and the other does not, and they are otherwise equal,
for (let index = 0; index < Math.max(aVerArr.length, bVerArr.length); index++) {
/* If one version number has more digits and the other does not, and they are otherwise equal,
assume the longer is greater. E.g. 1.1.1 > 1.1 */
if (index === aVerArr.length || aVerArr[index] < bVerArr[index]) {
return -1;
Expand All @@ -155,10 +140,10 @@ export const compareVersionNumbers: (a: string, b: string) => number | null = (
* @param metadata
* @param version
*/
export const versionIsAtLeast: (
metadata: ImplementationMetadata,
version: string
) => boolean | null = (metadata, version) => {
export const versionIsAtLeast: (metadata: ImplementationMetadata, version: string) => boolean | null = (
metadata,
version
) => {
let comparison = compareVersionNumbers(metadata.fdc3Version, version);
return comparison === null ? null : comparison >= 0 ? true : false;
};
20 changes: 4 additions & 16 deletions src/context/ContextTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,9 @@ export class Convert {

function invalidValue(typ: any, val: any, key: any = ''): never {
if (key) {
throw Error(
`Invalid value for key "${key}". Expected type ${JSON.stringify(
typ
)} but got ${JSON.stringify(val)}`
);
throw Error(`Invalid value for key "${key}". Expected type ${JSON.stringify(typ)} but got ${JSON.stringify(val)}`);
}
throw Error(
`Invalid value ${JSON.stringify(val)} for type ${JSON.stringify(typ)}`
);
throw Error(`Invalid value ${JSON.stringify(val)} for type ${JSON.stringify(typ)}`);
}

function jsonToJSProps(typ: any): any {
Expand Down Expand Up @@ -249,20 +243,14 @@ function transform(val: any, typ: any, getProps: any, key: any = ''): any {
return d;
}

function transformObject(
props: { [k: string]: any },
additional: any,
val: any
): any {
function transformObject(props: { [k: string]: any }, additional: any, val: any): any {
if (val === null || typeof val !== 'object' || Array.isArray(val)) {
return invalidValue('object', val);
}
const result: any = {};
Object.getOwnPropertyNames(props).forEach(key => {
const prop = props[key];
const v = Object.prototype.hasOwnProperty.call(val, key)
? val[key]
: undefined;
const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined;
result[prop.key] = transform(v, prop.typ, getProps, prop.key);
});
Object.getOwnPropertyNames(val).forEach(key => {
Expand Down
Loading