diff --git a/CHANGELOG.md b/CHANGELOG.md index cfbf2dee0..e7751b67f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Updated copyright notices ([#467](https://github.com/finos/FDC3/pull/467)) * Adjusted wording in API spec and documentation to acknowledge the possibility of methods of intent resolution other than a resolver UI ([#461](https://github.com/finos/FDC3/pull/461)) * Moved the Icon type definition into the Types documentation page for consistency with other types. ([#493](https://github.com/finos/FDC3/pull/493) +* All DesktopAgent and Channel API functions are now async for consistency, changing the return type of the `broadcast`, `addIntentListener`, `addContextListener` and `getInfo` functions ([#516](https://github.com/finos/FDC3/pull/516)) ### Deprecated ### Fixed diff --git a/docs/api/overview.md b/docs/api/overview.md index 11d302f1d..043421b6b 100644 --- a/docs/api/overview.md +++ b/docs/api/overview.md @@ -66,7 +66,7 @@ import { fdc3Ready, addIntentListener } from '@finos/fdc3' await fdc3Ready(); -const listener = addIntentListener('ViewAnalysis', instrument => { +const listener = await addIntentListener('ViewAnalysis', instrument => { // handle intent }) ``` diff --git a/docs/api/ref/Channel.md b/docs/api/ref/Channel.md index 71717405f..5de6cb9ed 100644 --- a/docs/api/ref/Channel.md +++ b/docs/api/ref/Channel.md @@ -20,13 +20,13 @@ interface Channel { displayMetadata?: DisplayMetadata; // methods - broadcast(context: Context): void; + broadcast(context: Context): Promise; getCurrentContext(contextType?: string): Promise; - addContextListener(contextType: string | null, handler: ContextHandler): Listener; + addContextListener(contextType: string | null, handler: ContextHandler): Promise; /** * @deprecated Use `addContextListener(null, handler)` instead of `addContextListener(handler)` */ - addContextListener(handler: ContextHandler): Listener; + addContextListener(handler: ContextHandler): Promise; } ``` @@ -72,7 +72,7 @@ DisplayMetadata can be used to provide display hints for channels intended to be ### `addContextListener` ```ts -public addContextListener(contextType: string | null, handler: ContextHandler): Listener; +public addContextListener(contextType: string | null, handler: ContextHandler): Promise; ``` Adds a listener for incoming contexts of the specified _context type_ whenever a broadcast happens on this channel. @@ -80,7 +80,7 @@ Adds a listener for incoming contexts of the specified _context type_ whenever a /** * @deprecated Use `addContextListener(null, handler)` instead of `addContextListener(handler)` */ -public addContextListener(handler: ContextHandler): Listener; +public addContextListener(handler: ContextHandler): Promise; ``` Adds a listener for incoming contexts whenever a broadcast happens on the channel. @@ -90,7 +90,7 @@ Adds a listener for incoming contexts whenever a broadcast happens on the channe Add a listener for any context that is broadcast on the channel: ```ts -const listener = channel.addContextListener(null, context => { +const listener = await channel.addContextListener(null, context => { if (context.type === 'fdc3.contact') { // handle the contact } else if (context.type === 'fdc3.instrument') => { @@ -105,11 +105,11 @@ listener.unsubscribe(); Adding listeners for specific types of context that is broadcast on the channel: ```ts -const contactListener = channel.addContextListener('fdc3.contact', contact => { +const contactListener = await channel.addContextListener('fdc3.contact', contact => { // handle the contact }); -const instrumentListener = channel.addContextListener('fdc3.instrument', instrument => { +const instrumentListener = await channel.addContextListener('fdc3.instrument', instrument => { // handle the instrument }); @@ -127,7 +127,7 @@ instrumentListener.unsubscribe(); ### `broadcast` ```typescript -public broadcast(context: Context): void; +public broadcast(context: Context): Promise; ``` Broadcasts a context on the channel. This function can be used without first joining the channel, allowing applications to broadcast on channels that they aren't a member of. diff --git a/docs/api/ref/DesktopAgent.md b/docs/api/ref/DesktopAgent.md index f7b52efdd..f0606d41d 100644 --- a/docs/api/ref/DesktopAgent.md +++ b/docs/api/ref/DesktopAgent.md @@ -18,19 +18,19 @@ interface DesktopAgent { open(app: TargetApp, context?: Context): Promise; // context - broadcast(context: Context): void; - addContextListener(contextType: string | null, handler: ContextHandler): Listener; + broadcast(context: Context): Promise; + addContextListener(contextType: string | null, handler: ContextHandler): Promise; /** * @deprecated 'Use `addContextListener(null, handler)` instead of `addContextListener(handler)` */ - addContextListener(handler: ContextHandler): Listener; + addContextListener(handler: ContextHandler): Promise; // intents findIntent(intent: string, context?: Context): Promise; findIntentsByContext(context: Context): Promise>; raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; raiseIntentForContext(context: Context, app?: TargetApp): Promise; - addIntentListener(intent: string, handler: ContextHandler): Listener; + addIntentListener(intent: string, handler: ContextHandler): Promise; // channels getOrCreateChannel(channelId: string): Promise; @@ -40,7 +40,7 @@ interface DesktopAgent { leaveCurrentChannel() : Promise; //implementation info - getInfo(): ImplementationMetadata; + getInfo(): Promise; } ``` @@ -49,11 +49,11 @@ interface DesktopAgent { ### `addContextListener` ```ts -addContextListener(contextType: string | null, handler: ContextHandler): Listener; +addContextListener(contextType: string | null, handler: ContextHandler): Promise; /** * @deprecated 'Use `addContextListener(null, handler)` instead of `addContextListener(handler)` */ -addContextListener(handler: ContextHandler): Listener; +addContextListener(handler: ContextHandler): Promise; ``` Adds a listener for incoming context broadcasts from the Desktop Agent. If the consumer is only interested in a context of a particular type, they can specify that type. If the consumer is able to receive context of any type or will inspect types received, then they can pass `null` as the `contextType` parameter to receive all context types. @@ -62,10 +62,10 @@ Context broadcasts are only received from apps that are joined to the same chann #### Examples ```js // any context -const listener = fdc3.addContextListener(null, context => { ... }); +const listener = await fdc3.addContextListener(null, context => { ... }); // listener for a specific type -const contactListener = fdc3.addContextListener('fdc3.contact', contact => { ... }); +const contactListener = await fdc3.addContextListener('fdc3.contact', contact => { ... }); ``` #### See also @@ -77,14 +77,14 @@ const contactListener = fdc3.addContextListener('fdc3.contact', contact => { ... ### `addIntentListener` ```ts -addIntentListener(intent: string, handler: ContextHandler): Listener; +addIntentListener(intent: string, handler: ContextHandler): Promise; ``` Adds a listener for incoming Intents from the Agent. #### Examples ```js -const listener = fdc3.addIntentListener('StartChat', context => { +const listener = await fdc3.addIntentListener('StartChat', context => { // start chat has been requested by another application }); ``` @@ -98,7 +98,7 @@ const listener = fdc3.addIntentListener('StartChat', context => { ### `broadcast` ```ts -broadcast(context: Context): void; +broadcast(context: Context): Promise; ``` Publishes context to other apps on the desktop. Calling `broadcast` at the `DesktopAgent` scope will push the context to whatever `Channel` the app is joined to. If the app is not currently joined to a channel, calling `fdc3.broadcast` will have no effect. Apps can still directly broadcast and listen to context on any channel via the methods on the `Channel` class. @@ -219,7 +219,7 @@ let current = await fdc3.getCurrentChannel(); ### `getInfo` ```ts -getInfo(): ImplementationMetadata; +getInfo(): Promise; ``` Retrieves information about the FDC3 Desktop Agent implementation, such as the implemented version of the FDC3 specification and the name of the implementation provider. @@ -231,7 +231,7 @@ Returns an [`ImplementationMetadata`](Metadata#implementationmetadata) object. ```js import {compareVersionNumbers, versionIsAtLeast} from '@finos/fdc3'; -if (fdc3.getInfo && versionIsAtLeast(fdc3.getInfo(), "1.2")) { +if (fdc3.getInfo && versionIsAtLeast(await fdc3.getInfo(), "1.2")) { await fdc3.raiseIntentForContext(context); } else { await fdc3.raiseIntent("ViewChart", context); diff --git a/docs/api/spec.md b/docs/api/spec.md index 5c0a9b309..4f304c90d 100644 --- a/docs/api/spec.md +++ b/docs/api/spec.md @@ -205,7 +205,7 @@ From version 1.2 of the FDC3 specification, it is possible to retrieve informati ```js import {compareVersionNumbers, versionIsAtLeast} from '@finos/fdc3'; -if (fdc3.getInfo && versionIsAtLeast(fdc3.getInfo(), '1.2')) { +if (fdc3.getInfo && versionIsAtLeast(await fdc3.getInfo(), '1.2')) { await fdc3.raiseIntentForContext(context); } else { await fdc3.raiseIntent('ViewChart', context); @@ -321,9 +321,9 @@ const appChannel = await fdc3.getOrCreateChannel('my_custom_channel'); // get the current context of the channel const current = await appChannel.getCurrentContext(); // add a listener -appChannel.addContextListener(null, context => {...}); +await appChannel.addContextListener(null, context => {...}); // broadcast to the channel -appChannel.broadcast(context); +await appChannel.broadcast(context); ``` diff --git a/src/api/Channel.ts b/src/api/Channel.ts index 86a96d14a..f4a09a842 100644 --- a/src/api/Channel.ts +++ b/src/api/Channel.ts @@ -40,7 +40,7 @@ export interface Channel { * * `Error` with a string from the `ChannelError` enumeration. */ - broadcast(context: Context): void; + broadcast(context: Context): Promise; /** * Returns the last context that was broadcast on this channel. All channels initially have no context, until a @@ -63,10 +63,10 @@ export interface Channel { * Adds a listener for incoming contexts whenever a broadcast happens on this channel. * @deprecated use `addContextListener(null, handler)` instead of `addContextListener(handler)`. */ - addContextListener(handler: ContextHandler): Listener; + addContextListener(handler: ContextHandler): Promise; /** * 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): Promise; } diff --git a/src/api/DesktopAgent.ts b/src/api/DesktopAgent.ts index 6b449a6f6..997c3b1f2 100644 --- a/src/api/DesktopAgent.ts +++ b/src/api/DesktopAgent.ts @@ -117,7 +117,7 @@ export interface DesktopAgent { * fdc3.broadcast(context); * ``` */ - broadcast(context: Context): void; + broadcast(context: Context): Promise; /** * Raises a specific intent for resolution against apps registered with the desktop agent. @@ -169,13 +169,13 @@ export interface DesktopAgent { /** * Adds a listener for incoming Intents from the Agent. */ - addIntentListener(intent: string, handler: ContextHandler): Listener; + addIntentListener(intent: string, handler: ContextHandler): Promise; /** * Adds a listener for incoming context broadcast from the Desktop Agent. * @deprecated use `addContextListener(null, handler)` instead of `addContextListener(handler)`. */ - addContextListener(handler: ContextHandler): Listener; + addContextListener(handler: ContextHandler): Promise; /** * Adds a listener for incoming context broadcasts from the Desktop Agent. If the consumer is only interested in a context of a particular type, they can they can specify that type. If the consumer is able to receive context of any type or will inspect types received, then they can pass `null` as the `contextType` parameter to receive all context types. @@ -187,7 +187,7 @@ export interface DesktopAgent { * const contactListener = fdc3.addContextListener('fdc3.contact', contact => { ... }); * ``` */ - addContextListener(contextType: string | null, handler: ContextHandler): Listener; + addContextListener(contextType: string | null, handler: ContextHandler): Promise; /** * Retrieves a list of the System channels available for the app to join @@ -243,5 +243,5 @@ export interface DesktopAgent { * the implemented version of the FDC3 specification and the name of the implementation * provider. */ - getInfo(): ImplementationMetadata; + getInfo(): Promise; } diff --git a/src/api/Methods.ts b/src/api/Methods.ts index 631769eac..56289da38 100644 --- a/src/api/Methods.ts +++ b/src/api/Methods.ts @@ -11,13 +11,21 @@ function rejectIfNoGlobal(f: () => Promise) { return window.fdc3 ? f() : Promise.reject(UnavailableError); } -function throwIfNoGlobal(f: () => any) { - if (!window.fdc3) { - throw UnavailableError; - } - return f(); -} - +/** + * Utility function that returns a promise that will resolve immeadiately + * if the desktop agent API is found at `window.fdc3`. If the API is found, + * the promise will resolve when the `fdc3Ready` event is received or if it + * is found at the end of the specified timeout. If the API is not found, it + * will reject with an error. + * + * ```javascript + * await fdc3Ready(); + * const intentListener = await addIntentListener("ViewChart", intentHandlerFn); + * ``` + * + * @param waitForMs The number of milliseconds to wait for the FDC3 API to be + * ready. Defaults to 5 seconds. + */ export const fdc3Ready = async (waitForMs = DEFAULT_TIMEOUT): Promise => { return new Promise((resolve, reject) => { // if the global is already available resolve immediately @@ -51,8 +59,8 @@ export function findIntentsByContext(context: Context): Promise { return rejectIfNoGlobal(() => window.fdc3.findIntentsByContext(context)); } -export function broadcast(context: Context): void { - throwIfNoGlobal(() => window.fdc3.broadcast(context)); +export function broadcast(context: Context): Promise { + return rejectIfNoGlobal(() => window.fdc3.broadcast(context)); } export function raiseIntent(intent: string, context: Context, app?: TargetApp): Promise { @@ -63,19 +71,19 @@ export function raiseIntentForContext(context: Context, app?: TargetApp): Promis return rejectIfNoGlobal(() => window.fdc3.raiseIntentForContext(context, app)); } -export function addIntentListener(intent: string, handler: ContextHandler): Listener { - return throwIfNoGlobal(() => window.fdc3.addIntentListener(intent, handler)); +export function addIntentListener(intent: string, handler: ContextHandler): Promise { + return rejectIfNoGlobal(() => window.fdc3.addIntentListener(intent, handler)); } export function addContextListener( contextTypeOrHandler: string | null | ContextHandler, handler?: ContextHandler -): Listener { +): Promise { //Handle (deprecated) function signature that allowed contextType argument to be omitted if (typeof contextTypeOrHandler !== 'function') { - return throwIfNoGlobal(() => window.fdc3.addContextListener(contextTypeOrHandler, handler as ContextHandler)); + return rejectIfNoGlobal(() => window.fdc3.addContextListener(contextTypeOrHandler, handler as ContextHandler)); } else { - return throwIfNoGlobal(() => window.fdc3.addContextListener(null, contextTypeOrHandler as ContextHandler)); + return rejectIfNoGlobal(() => window.fdc3.addContextListener(null, contextTypeOrHandler as ContextHandler)); } } @@ -99,8 +107,8 @@ export function leaveCurrentChannel(): Promise { return rejectIfNoGlobal(() => window.fdc3.leaveCurrentChannel()); } -export function getInfo(): ImplementationMetadata { - return throwIfNoGlobal(() => window.fdc3.getInfo()); +export function getInfo(): Promise { + return rejectIfNoGlobal(() => window.fdc3.getInfo()); } /** diff --git a/test/Methods.test.ts b/test/Methods.test.ts index 6d525f73d..57ad928a1 100644 --- a/test/Methods.test.ts +++ b/test/Methods.test.ts @@ -57,8 +57,8 @@ describe('test ES6 module', () => { await expect(findIntentsByContext(expect.any(Object))).rejects.toEqual(UnavailableError); }); - test('broadcast should throw', async () => { - expect(() => broadcast(expect.any(Object))).toThrowError(UnavailableError); + test('broadcast should reject', async () => { + await expect(broadcast(expect.any(Object))).rejects.toEqual(UnavailableError); }); test('raiseIntent should reject', async () => { @@ -69,14 +69,14 @@ describe('test ES6 module', () => { await expect(raiseIntentForContext(expect.any(Object))).rejects.toEqual(UnavailableError); }); - test('addIntentListener should throw', () => { - expect(() => addIntentListener(expect.any(String), expect.any(Function))).toThrowError(UnavailableError); + test('addIntentListener should reject', async () => { + await expect(addIntentListener(expect.any(String), expect.any(Function))).rejects.toEqual(UnavailableError); }); - test('addContextListener should throw', () => { - expect(() => addContextListener(expect.any(Object))).toThrowError(UnavailableError); + test('addContextListener should reject', async () => { + await expect(addContextListener(expect.any(Object))).rejects.toEqual(UnavailableError); - expect(() => addContextListener(expect.any(String), expect.any(Object))).toThrowError(UnavailableError); + await expect(addContextListener(expect.any(String), expect.any(Object))).rejects.toEqual(UnavailableError); }); test('getSystemChannels should reject', async () => { @@ -99,8 +99,8 @@ describe('test ES6 module', () => { await expect(leaveCurrentChannel()).rejects.toEqual(UnavailableError); }); - test('getInfo should throw', () => { - expect(() => getInfo()).toThrowError(UnavailableError); + test('getInfo should reject', async () => { + await expect(() => getInfo()).rejects.toEqual(UnavailableError); }); }); @@ -138,8 +138,8 @@ describe('test ES6 module', () => { expect(window.fdc3.findIntentsByContext).toHaveBeenCalledWith(ContactContext); }); - test('broadcast should delegate to window.fdc3.broadcast', () => { - broadcast(ContactContext); + test('broadcast should delegate to window.fdc3.broadcast', async () => { + await broadcast(ContactContext); expect(window.fdc3.broadcast).toHaveBeenCalledTimes(1); expect(window.fdc3.broadcast).toHaveBeenCalledWith(ContactContext); @@ -164,23 +164,23 @@ describe('test ES6 module', () => { expect(window.fdc3.raiseIntentForContext).toHaveBeenCalledWith(ContactContext, app); }); - test('addIntentListener should delegate to window.fdc3.addIntentListener', () => { + test('addIntentListener should delegate to window.fdc3.addIntentListener', async () => { const intent = 'ViewChart'; const handler: ContextHandler = _ => {}; - addIntentListener(intent, handler); + await addIntentListener(intent, handler); expect(window.fdc3.addIntentListener).toHaveBeenCalledTimes(1); expect(window.fdc3.addIntentListener).toHaveBeenCalledWith(intent, handler); }); - test('addContextListener should delegate to window.fdc3.addContextListener', () => { + test('addContextListener should delegate to window.fdc3.addContextListener', async () => { const type = 'fdc3.instrument'; const handler1: ContextHandler = _ => {}; const handler2: ContextHandler = _ => {}; - addContextListener(type, handler1); - addContextListener(handler2); + await addContextListener(type, handler1); + await addContextListener(handler2); expect(window.fdc3.addContextListener).toHaveBeenCalledTimes(2); expect(window.fdc3.addContextListener).toHaveBeenNthCalledWith(1, type, handler1); @@ -226,8 +226,8 @@ describe('test ES6 module', () => { expect(window.fdc3.leaveCurrentChannel).toHaveBeenCalledWith(); }); - test('getInfo should delegate to window.fdc3.getInfo', () => { - getInfo(); + test('getInfo should delegate to window.fdc3.getInfo', async () => { + await getInfo(); expect(window.fdc3.getInfo).toHaveBeenCalledTimes(1); expect(window.fdc3.getInfo).toHaveBeenCalledWith();