Skip to content

Commit 7ffe04b

Browse files
authored
Merge pull request #360 from rikoe/add-fdc3ready-function
feat: add fdc3Ready helper function and getInfo to npm package exports
2 parents ff1314e + 0d3fdb6 commit 7ffe04b

11 files changed

+340
-294
lines changed

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
# ignore folders
99
website
1010
dist
11+
src/app-directory/*/target

docs/api/overview.md

+11-9
Original file line numberDiff line numberDiff line change
@@ -53,24 +53,26 @@ The [`@finos/fdc3` npm package](https://www.npmjs.com/package/@finos/fdc3) provi
5353
```ts
5454
import * as fdc3 from '@finos/fdc3'
5555

56-
const listener = fdc3.addIntentListener('ViewAnalysis', context => {
57-
// do something
56+
await fdc3.raiseIntent('ViewAnalysis', {
57+
type: 'fdc3.instrument',
58+
id: { ticker: 'AAPL' }
5859
})
5960
```
6061

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

6364
```ts
64-
import { raiseIntent } from '@finos/fdc3'
65+
import { fdc3Ready, addIntentListener } from '@finos/fdc3'
6566

66-
await raiseIntent('ViewAnalysis', {
67-
type: 'fdc3.instrument',
68-
id: { ticker: 'AAPL' }
67+
await fdc3Ready();
68+
69+
const listener = addIntentListener('ViewAnalysis', instrument => {
70+
// handle intent
6971
})
7072
```
7173

72-
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.
73-
74+
#### See also
75+
* [`fdc3Ready() Function`](ref/Globals#fdc3ready-function)
7476

7577

7678

docs/api/ref/Globals.md

+28-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,33 @@ function fdc3Action() {
3535
if (window.fdc3) {
3636
fdc3Action();
3737
} else {
38-
window.addEventListener("fdc3Ready", fdc3Action);
38+
window.addEventListener('fdc3Ready', fdc3Action);
3939
}
4040
```
41+
42+
## `fdc3Ready()` Function
43+
44+
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.
45+
46+
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).
47+
48+
### Example
49+
50+
```ts
51+
import { fdc3Ready, broadcast } from '@finos/fdc3'
52+
53+
async function fdc3Action() {
54+
try {
55+
await fdc3Ready(1000); // wait for (at most) 1 second
56+
broadcast({
57+
type: 'fdc3.instrument',
58+
id: { ticker: 'AAPL' }
59+
})
60+
} catch (error) {
61+
// handle error
62+
}
63+
}
64+
```
65+
66+
67+

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
"singleQuote": true,
3838
"arrowParens": "avoid",
3939
"trailingComma": "es5",
40-
"endOfLine": "auto"
40+
"endOfLine": "auto",
41+
"printWidth": 120
4142
},
4243
"resolutions": {
4344
"node-fetch": "^2.6.1",
@@ -47,6 +48,7 @@
4748
},
4849
"devDependencies": {
4950
"husky": "^4.3.0",
51+
"jest-mock-extended": "^1.0.13",
5052
"quicktype": "^15.0.258",
5153
"tsdx": "^0.14.1",
5254
"tslib": "^2.0.1",

src/api/Channel.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,5 @@ export interface Channel {
6868
/**
6969
* Adds a listener for incoming contexts of the specified context type whenever a broadcast happens on this channel.
7070
*/
71-
addContextListener(
72-
contextType: string | null,
73-
handler: ContextHandler
74-
): Listener;
71+
addContextListener(contextType: string | null, handler: ContextHandler): Listener;
7572
}

src/api/DesktopAgent.ts

+3-13
Original file line numberDiff line numberDiff line change
@@ -124,22 +124,15 @@ export interface DesktopAgent {
124124
* await fdc3.raiseIntent("StartChat", context, appMetadata);
125125
* ```
126126
*/
127-
raiseIntent(
128-
intent: string,
129-
context: Context,
130-
app?: TargetApp
131-
): Promise<IntentResolution>;
127+
raiseIntent(intent: string, context: Context, app?: TargetApp): Promise<IntentResolution>;
132128

133129
/**
134130
* Raises a context to the desktop agent to resolve with one of the possible Intents for that context.
135131
* ```javascript
136132
* await fdc3.raiseIntentForContext(context);
137133
* ```
138134
*/
139-
raiseIntentForContext(
140-
context: Context,
141-
app?: TargetApp
142-
): Promise<IntentResolution>;
135+
raiseIntentForContext(context: Context, app?: TargetApp): Promise<IntentResolution>;
143136

144137
/**
145138
* Adds a listener for incoming Intents from the Agent.
@@ -155,10 +148,7 @@ export interface DesktopAgent {
155148
/**
156149
* Adds a listener for the broadcast of a specific type of context object.
157150
*/
158-
addContextListener(
159-
contextType: string | null,
160-
handler: ContextHandler
161-
): Listener;
151+
addContextListener(contextType: string | null, handler: ContextHandler): Listener;
162152

163153
/**
164154
* Retrieves a list of the System channels available for the app to join

src/api/Methods.ts

+73-88
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,105 @@
1-
import {
2-
AppIntent,
3-
Channel,
4-
Context,
5-
ContextHandler,
6-
IntentResolution,
7-
Listener,
8-
ImplementationMetadata,
9-
} from '..';
1+
import { AppIntent, Channel, Context, ContextHandler, IntentResolution, Listener, ImplementationMetadata } from '..';
102
import { TargetApp } from './Types';
113

12-
const unavailableError = new Error(
13-
'FDC3 DesktopAgent not available at `window.fdc3`.'
14-
);
4+
const DEFAULT_TIMEOUT = 5000;
155

16-
const rejectIfNoGlobal = (f: () => Promise<any>) => {
17-
return window.fdc3 ? f() : Promise.reject(unavailableError);
18-
};
6+
const UnavailableError = new Error('FDC3 DesktopAgent not available at `window.fdc3`.');
7+
const TimeoutError = new Error('Timed out waiting for `fdc3Ready` event.');
8+
const UnexpectedError = new Error('`fdc3Ready` event fired, but `window.fdc3` not set to DesktopAgent.');
9+
10+
function rejectIfNoGlobal(f: () => Promise<any>) {
11+
return window.fdc3 ? f() : Promise.reject(UnavailableError);
12+
}
1913

20-
const throwIfNoGlobal = (f: () => any) => {
14+
function throwIfNoGlobal(f: () => any) {
2115
if (!window.fdc3) {
22-
throw unavailableError;
16+
throw UnavailableError;
2317
}
2418
return f();
19+
}
20+
21+
export const fdc3Ready = async (waitForMs = DEFAULT_TIMEOUT): Promise<void> => {
22+
return new Promise((resolve, reject) => {
23+
// if the global is already available resolve immediately
24+
if (window.fdc3) {
25+
resolve();
26+
} else {
27+
// if its not available setup a timeout to return a rejected promise
28+
const timeout = setTimeout(() => (window.fdc3 ? resolve() : reject(TimeoutError)), waitForMs);
29+
// listen for the fdc3Ready event
30+
window.addEventListener(
31+
'fdc3Ready',
32+
() => {
33+
clearTimeout(timeout);
34+
window.fdc3 ? resolve() : reject(UnexpectedError);
35+
},
36+
{ once: true }
37+
);
38+
}
39+
});
2540
};
2641

27-
export const open: (app: TargetApp, context?: Context) => Promise<void> = (
28-
app,
29-
context
30-
) => {
42+
export function open(app: TargetApp, context?: Context): Promise<void> {
3143
return rejectIfNoGlobal(() => window.fdc3.open(app, context));
32-
};
44+
}
3345

34-
export const findIntent: (
35-
intent: string,
36-
context?: Context
37-
) => Promise<AppIntent> = (intent, context) => {
46+
export function findIntent(intent: string, context?: Context): Promise<AppIntent> {
3847
return rejectIfNoGlobal(() => window.fdc3.findIntent(intent, context));
39-
};
48+
}
4049

41-
export const findIntentsByContext: (
42-
context: Context
43-
) => Promise<Array<AppIntent>> = context => {
50+
export function findIntentsByContext(context: Context): Promise<AppIntent[]> {
4451
return rejectIfNoGlobal(() => window.fdc3.findIntentsByContext(context));
45-
};
52+
}
4653

47-
export const broadcast: (context: Context) => void = context => {
54+
export function broadcast(context: Context): void {
4855
throwIfNoGlobal(() => window.fdc3.broadcast(context));
49-
};
56+
}
5057

51-
export const raiseIntent: (
52-
intent: string,
53-
context: Context,
54-
app?: TargetApp
55-
) => Promise<IntentResolution> = (intent, context, app) => {
58+
export function raiseIntent(intent: string, context: Context, app?: TargetApp): Promise<IntentResolution> {
5659
return rejectIfNoGlobal(() => window.fdc3.raiseIntent(intent, context, app));
57-
};
60+
}
5861

59-
export const raiseIntentForContext: (
60-
context: Context,
61-
app?: TargetApp
62-
) => Promise<IntentResolution> = (context, app) => {
63-
return rejectIfNoGlobal(() =>
64-
window.fdc3.raiseIntentForContext(context, app)
65-
);
66-
};
62+
export function raiseIntentForContext(context: Context, app?: TargetApp): Promise<IntentResolution> {
63+
return rejectIfNoGlobal(() => window.fdc3.raiseIntentForContext(context, app));
64+
}
6765

68-
export const addIntentListener: (
69-
intent: string,
70-
handler: ContextHandler
71-
) => Listener = (intent, handler) => {
66+
export function addIntentListener(intent: string, handler: ContextHandler): Listener {
7267
return throwIfNoGlobal(() => window.fdc3.addIntentListener(intent, handler));
73-
};
68+
}
7469

75-
export const addContextListener: (
76-
contextTypeOrHandler: string | ContextHandler,
77-
handler?: ContextHandler
78-
) => Listener = (a, b) => {
79-
if (typeof a !== 'function') {
70+
export function addContextListener(contextTypeOrHandler: string | ContextHandler, handler?: ContextHandler): Listener {
71+
if (typeof contextTypeOrHandler !== 'function') {
8072
return throwIfNoGlobal(() =>
81-
window.fdc3.addContextListener(a as string, b as ContextHandler)
73+
window.fdc3.addContextListener(contextTypeOrHandler as string, handler as ContextHandler)
8274
);
8375
} else {
84-
return throwIfNoGlobal(() =>
85-
window.fdc3.addContextListener(a as ContextHandler)
86-
);
76+
return throwIfNoGlobal(() => window.fdc3.addContextListener(contextTypeOrHandler as ContextHandler));
8777
}
88-
};
78+
}
8979

90-
export const getSystemChannels: () => Promise<Array<Channel>> = () => {
80+
export function getSystemChannels(): Promise<Channel[]> {
9181
return rejectIfNoGlobal(() => window.fdc3.getSystemChannels());
92-
};
82+
}
9383

94-
export const joinChannel: (channelId: string) => Promise<void> = channelId => {
84+
export function joinChannel(channelId: string): Promise<void> {
9585
return rejectIfNoGlobal(() => window.fdc3.joinChannel(channelId));
96-
};
86+
}
9787

98-
export const getOrCreateChannel: (
99-
channelId: string
100-
) => Promise<Channel> = channelId => {
88+
export function getOrCreateChannel(channelId: string): Promise<Channel> {
10189
return rejectIfNoGlobal(() => window.fdc3.getOrCreateChannel(channelId));
102-
};
90+
}
10391

104-
export const getCurrentChannel: () => Promise<Channel | null> = () => {
92+
export function getCurrentChannel(): Promise<Channel | null> {
10593
return rejectIfNoGlobal(() => window.fdc3.getCurrentChannel());
106-
};
94+
}
10795

108-
export const leaveCurrentChannel: () => Promise<void> = () => {
96+
export function leaveCurrentChannel(): Promise<void> {
10997
return rejectIfNoGlobal(() => window.fdc3.leaveCurrentChannel());
110-
};
98+
}
99+
100+
export function getInfo(): ImplementationMetadata {
101+
return throwIfNoGlobal(() => window.fdc3.getInfo());
102+
}
111103

112104
/**
113105
* Compare numeric semver version number strings (in the form `1.2.3`).
@@ -119,19 +111,12 @@ export const leaveCurrentChannel: () => Promise<void> = () => {
119111
* @param a
120112
* @param b
121113
*/
122-
export const compareVersionNumbers: (a: string, b: string) => number | null = (
123-
a,
124-
b
125-
) => {
114+
export const compareVersionNumbers: (a: string, b: string) => number | null = (a, b) => {
126115
try {
127116
let aVerArr = a.split('.').map(Number);
128117
let bVerArr = b.split('.').map(Number);
129-
for (
130-
let index = 0;
131-
index < Math.max(aVerArr.length, bVerArr.length);
132-
index++
133-
) {
134-
/* If one version number has more digits and the other does not, and they are otherwise equal,
118+
for (let index = 0; index < Math.max(aVerArr.length, bVerArr.length); index++) {
119+
/* If one version number has more digits and the other does not, and they are otherwise equal,
135120
assume the longer is greater. E.g. 1.1.1 > 1.1 */
136121
if (index === aVerArr.length || aVerArr[index] < bVerArr[index]) {
137122
return -1;
@@ -155,10 +140,10 @@ export const compareVersionNumbers: (a: string, b: string) => number | null = (
155140
* @param metadata
156141
* @param version
157142
*/
158-
export const versionIsAtLeast: (
159-
metadata: ImplementationMetadata,
160-
version: string
161-
) => boolean | null = (metadata, version) => {
143+
export const versionIsAtLeast: (metadata: ImplementationMetadata, version: string) => boolean | null = (
144+
metadata,
145+
version
146+
) => {
162147
let comparison = compareVersionNumbers(metadata.fdc3Version, version);
163148
return comparison === null ? null : comparison >= 0 ? true : false;
164149
};

src/context/ContextTypes.ts

+4-16
Original file line numberDiff line numberDiff line change
@@ -180,15 +180,9 @@ export class Convert {
180180

181181
function invalidValue(typ: any, val: any, key: any = ''): never {
182182
if (key) {
183-
throw Error(
184-
`Invalid value for key "${key}". Expected type ${JSON.stringify(
185-
typ
186-
)} but got ${JSON.stringify(val)}`
187-
);
183+
throw Error(`Invalid value for key "${key}". Expected type ${JSON.stringify(typ)} but got ${JSON.stringify(val)}`);
188184
}
189-
throw Error(
190-
`Invalid value ${JSON.stringify(val)} for type ${JSON.stringify(typ)}`
191-
);
185+
throw Error(`Invalid value ${JSON.stringify(val)} for type ${JSON.stringify(typ)}`);
192186
}
193187

194188
function jsonToJSProps(typ: any): any {
@@ -249,20 +243,14 @@ function transform(val: any, typ: any, getProps: any, key: any = ''): any {
249243
return d;
250244
}
251245

252-
function transformObject(
253-
props: { [k: string]: any },
254-
additional: any,
255-
val: any
256-
): any {
246+
function transformObject(props: { [k: string]: any }, additional: any, val: any): any {
257247
if (val === null || typeof val !== 'object' || Array.isArray(val)) {
258248
return invalidValue('object', val);
259249
}
260250
const result: any = {};
261251
Object.getOwnPropertyNames(props).forEach(key => {
262252
const prop = props[key];
263-
const v = Object.prototype.hasOwnProperty.call(val, key)
264-
? val[key]
265-
: undefined;
253+
const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined;
266254
result[prop.key] = transform(v, prop.typ, getProps, prop.key);
267255
});
268256
Object.getOwnPropertyNames(val).forEach(key => {

0 commit comments

Comments
 (0)