@openfin/cloud-interop-core-api 0.0.1-alpha.e8aa2c9 → 0.0.1-alpha.e9e4a55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/bundle.d.ts +11 -1
  2. package/index.cjs +43 -23
  3. package/index.mjs +43 -23
  4. package/package.json +2 -2
package/bundle.d.ts CHANGED
@@ -268,7 +268,7 @@ export declare class CloudInteropAPI {
268
268
  * @returns {*} {Promise<void>}
269
269
  * @memberof CloudInteropAPI
270
270
  */
271
- setContext(contextGroup: string, context: object): Promise<void>;
271
+ setContext(contextGroup: string, context: InferredContext): Promise<void>;
272
272
  /**
273
273
  * Starts an intent discovery operation
274
274
  *
@@ -420,6 +420,14 @@ export declare type CreateSessionResponse = {
420
420
  sourceId: string;
421
421
  };
422
422
 
423
+ declare const errorSchema: z.ZodObject<{
424
+ error: z.ZodString;
425
+ }, "strip", z.ZodTypeAny, {
426
+ error: string;
427
+ }, {
428
+ error: string;
429
+ }>;
430
+
423
431
  export declare type EventListenersMap = Map<keyof EventMap, Array<(...args: Parameters<EventMap[keyof EventMap]>) => void>>;
424
432
 
425
433
  export declare type EventMap = {
@@ -545,6 +553,8 @@ declare type HistoryRecord = {
545
553
  */
546
554
  export declare type InferredContext = z.infer<typeof contextSchema>;
547
555
 
556
+ export declare type InferredError = z.infer<typeof errorSchema>;
557
+
548
558
  /**
549
559
  * @internal
550
560
  */
package/index.cjs CHANGED
@@ -46,6 +46,8 @@ class EventController {
46
46
  }
47
47
  }
48
48
 
49
+ const isErrorIntentResult = (result) => 'error' in result;
50
+
49
51
  const APP_ID_DELIM = '::';
50
52
  const getRequestHeaders = (connectionParameters) => {
51
53
  const headers = {};
@@ -72,13 +74,9 @@ const getRequestHeaders = (connectionParameters) => {
72
74
  * @param source
73
75
  * @returns
74
76
  */
75
- const encodeAppIntents = (appIntents, { sessionId, sourceId }) => appIntents.map((intent) => ({
77
+ const encodeAppIntents = (appIntents, source) => appIntents.map((intent) => ({
76
78
  ...intent,
77
- apps: intent.apps.map((app) => {
78
- const id = encodeURIComponent(app.appId);
79
- const sId = encodeURIComponent(sourceId);
80
- return { ...app, appId: `${id}${APP_ID_DELIM}${sId}${APP_ID_DELIM}${sessionId}` };
81
- }),
79
+ apps: intent.apps.map((app) => ({ ...app, appId: encodeAppId(app.appId, source) })),
82
80
  }));
83
81
  /**
84
82
  * Decodes all app intents by URI decoding the parts previously encoded by `encodeAppIntents`
@@ -87,13 +85,19 @@ const encodeAppIntents = (appIntents, { sessionId, sourceId }) => appIntents.map
87
85
  */
88
86
  const decodeAppIntents = (appIntents) => appIntents.map((intent) => ({
89
87
  ...intent,
90
- apps: intent.apps.map((app) => {
91
- const [encodedAppId, encodedSourceId, sessionId] = app.appId.split(APP_ID_DELIM);
92
- const id = decodeURIComponent(encodedAppId);
93
- const sourceId = decodeURIComponent(encodedSourceId);
94
- return { ...app, appId: `${id}${APP_ID_DELIM}${sourceId}${APP_ID_DELIM}${sessionId}` };
95
- }),
88
+ apps: intent.apps.map((app) => ({ ...app, appId: decodeAppId(app.appId) })),
96
89
  }));
90
+ const encodeAppId = (appIdString, { sessionId, sourceId }) => {
91
+ const id = encodeURIComponent(appIdString);
92
+ const sId = encodeURIComponent(sourceId);
93
+ return `${id}${APP_ID_DELIM}${sId}${APP_ID_DELIM}${sessionId}`;
94
+ };
95
+ const decodeAppId = (appId) => {
96
+ const [encodedAppId, encodedSourceId, sessionId] = appId.split(APP_ID_DELIM);
97
+ const id = decodeURIComponent(encodedAppId);
98
+ const sourceId = decodeURIComponent(encodedSourceId);
99
+ return `${id}${APP_ID_DELIM}${sourceId}${APP_ID_DELIM}${sessionId}`;
100
+ };
97
101
  /**
98
102
  * Decodes the AppIdentifier to extract the appId, sourceId, and sessionId.
99
103
  * @returns an object with:
@@ -158,13 +162,18 @@ class IntentController {
158
162
  body: JSON.stringify({ findOptions }),
159
163
  });
160
164
  if (!startResponse.ok) {
161
- throw new Error(startResponse.statusText);
165
+ throw new Error(`Error creating intent discovery record: ${startResponse.statusText}`);
162
166
  }
163
167
  // TODO: type this response?
164
168
  const json = await startResponse.json();
165
169
  this.#discovery.id = json.discoveryId;
166
170
  this.#discovery.sessionCount = json.sessionCount;
167
171
  this.#discovery.state = 'in-progress';
172
+ if (this.#discovery.sessionCount === 1) {
173
+ // since we have no other connected sessions, we can end discovery immediately
174
+ await this.#endIntentDiscovery(false);
175
+ return;
176
+ }
168
177
  // Listen out for discovery results directly sent to us
169
178
  await this.#mqttClient.subscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`);
170
179
  this.#discoveryTimeout = setTimeout(() => this.#endIntentDiscovery(), clampedTimeout);
@@ -175,10 +184,8 @@ class IntentController {
175
184
  throw new CloudInteropAPIError('Error starting intent discovery', 'ERR_STARTING_INTENT_DISCOVERY', error);
176
185
  }
177
186
  }
178
- async #endIntentDiscovery() {
187
+ async #endIntentDiscovery(mqttUnsubscribe = true) {
179
188
  if (this.#discovery.state !== 'in-progress') {
180
- // TODO: remove debug logs
181
- this.#logger('debug', 'Intent discovery not in progress');
182
189
  return;
183
190
  }
184
191
  if (this.#discoveryTimeout) {
@@ -188,10 +195,12 @@ class IntentController {
188
195
  this.#discovery.state = 'ended';
189
196
  // emit our aggregated events
190
197
  this.#events.emitEvent('aggregate-intent-details', { responses: this.#discovery.pendingIntentDetailsEvents });
191
- // gracefully end discovery
192
- await this.#mqttClient.unsubscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`).catch(() => {
193
- this.#logger('warn', `Error ending intent discovery: could not unsubscribe from discovery id ${this.#discovery.id}`);
194
- });
198
+ if (mqttUnsubscribe) {
199
+ // gracefully end discovery
200
+ await this.#mqttClient.unsubscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`).catch(() => {
201
+ this.#logger('warn', `Error ending intent discovery: could not unsubscribe from discovery id ${this.#discovery.id}`);
202
+ });
203
+ }
195
204
  await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/${this.#discovery.id}`, {
196
205
  method: 'DELETE',
197
206
  headers: getRequestHeaders(this.#connectionParams),
@@ -200,7 +209,6 @@ class IntentController {
200
209
  if (!deleteResponse.ok) {
201
210
  throw new Error(`Error ending intent discovery: ${deleteResponse.statusText}`);
202
211
  }
203
- this.#logger('debug', 'Intent discovery ended');
204
212
  })
205
213
  .catch((error) => {
206
214
  this.#logger('warn', `Error ending intent discovery: ${error}`);
@@ -244,6 +252,13 @@ class IntentController {
244
252
  return false;
245
253
  }
246
254
  async sendIntentResult(initiatingSessionId, result) {
255
+ if (!isErrorIntentResult(result)) {
256
+ // cloud-encode the source app id to support chained intent actions over cloud
257
+ // https://fdc3.finos.org/docs/2.0/api/spec#resolution-object -> "Use metadata about the resolving app instance to target a further intent"
258
+ const source = getSourceFromSession(this.#sessionDetails);
259
+ const encoded = encodeAppId(typeof result.source === 'string' ? result.source : result.source.appId, source);
260
+ result.source = typeof result.source === 'string' ? encoded : { ...result.source, appId: encoded };
261
+ }
247
262
  const { sessionId } = getSourceFromSession(this.#sessionDetails);
248
263
  const resultResponse = await fetch(`${this.#url}/api/intents/${initiatingSessionId}/result/${sessionId}`, {
249
264
  method: 'POST',
@@ -580,8 +595,13 @@ class CloudInteropAPI {
580
595
  if (contextEvent.source.sessionId === sessionDetails.sessionId) {
581
596
  return;
582
597
  }
583
- const { contextGroup, context, source, history } = contextEvent;
584
- this.#events.emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
598
+ const { context, payload, contextGroup, channelName, source, history } = contextEvent;
599
+ this.#events.emitEvent('context', {
600
+ contextGroup: channelName || contextGroup,
601
+ context: payload || context,
602
+ source,
603
+ history: { ...history, clientReceived: Date.now() },
604
+ });
585
605
  }
586
606
  else if (topic.startsWith(`${sessionDetails.sessionRootTopic}/commands`)) {
587
607
  this.#handleCommandMessage(messageEnvelope);
package/index.mjs CHANGED
@@ -44,6 +44,8 @@ class EventController {
44
44
  }
45
45
  }
46
46
 
47
+ const isErrorIntentResult = (result) => 'error' in result;
48
+
47
49
  const APP_ID_DELIM = '::';
48
50
  const getRequestHeaders = (connectionParameters) => {
49
51
  const headers = {};
@@ -70,13 +72,9 @@ const getRequestHeaders = (connectionParameters) => {
70
72
  * @param source
71
73
  * @returns
72
74
  */
73
- const encodeAppIntents = (appIntents, { sessionId, sourceId }) => appIntents.map((intent) => ({
75
+ const encodeAppIntents = (appIntents, source) => appIntents.map((intent) => ({
74
76
  ...intent,
75
- apps: intent.apps.map((app) => {
76
- const id = encodeURIComponent(app.appId);
77
- const sId = encodeURIComponent(sourceId);
78
- return { ...app, appId: `${id}${APP_ID_DELIM}${sId}${APP_ID_DELIM}${sessionId}` };
79
- }),
77
+ apps: intent.apps.map((app) => ({ ...app, appId: encodeAppId(app.appId, source) })),
80
78
  }));
81
79
  /**
82
80
  * Decodes all app intents by URI decoding the parts previously encoded by `encodeAppIntents`
@@ -85,13 +83,19 @@ const encodeAppIntents = (appIntents, { sessionId, sourceId }) => appIntents.map
85
83
  */
86
84
  const decodeAppIntents = (appIntents) => appIntents.map((intent) => ({
87
85
  ...intent,
88
- apps: intent.apps.map((app) => {
89
- const [encodedAppId, encodedSourceId, sessionId] = app.appId.split(APP_ID_DELIM);
90
- const id = decodeURIComponent(encodedAppId);
91
- const sourceId = decodeURIComponent(encodedSourceId);
92
- return { ...app, appId: `${id}${APP_ID_DELIM}${sourceId}${APP_ID_DELIM}${sessionId}` };
93
- }),
86
+ apps: intent.apps.map((app) => ({ ...app, appId: decodeAppId(app.appId) })),
94
87
  }));
88
+ const encodeAppId = (appIdString, { sessionId, sourceId }) => {
89
+ const id = encodeURIComponent(appIdString);
90
+ const sId = encodeURIComponent(sourceId);
91
+ return `${id}${APP_ID_DELIM}${sId}${APP_ID_DELIM}${sessionId}`;
92
+ };
93
+ const decodeAppId = (appId) => {
94
+ const [encodedAppId, encodedSourceId, sessionId] = appId.split(APP_ID_DELIM);
95
+ const id = decodeURIComponent(encodedAppId);
96
+ const sourceId = decodeURIComponent(encodedSourceId);
97
+ return `${id}${APP_ID_DELIM}${sourceId}${APP_ID_DELIM}${sessionId}`;
98
+ };
95
99
  /**
96
100
  * Decodes the AppIdentifier to extract the appId, sourceId, and sessionId.
97
101
  * @returns an object with:
@@ -156,13 +160,18 @@ class IntentController {
156
160
  body: JSON.stringify({ findOptions }),
157
161
  });
158
162
  if (!startResponse.ok) {
159
- throw new Error(startResponse.statusText);
163
+ throw new Error(`Error creating intent discovery record: ${startResponse.statusText}`);
160
164
  }
161
165
  // TODO: type this response?
162
166
  const json = await startResponse.json();
163
167
  this.#discovery.id = json.discoveryId;
164
168
  this.#discovery.sessionCount = json.sessionCount;
165
169
  this.#discovery.state = 'in-progress';
170
+ if (this.#discovery.sessionCount === 1) {
171
+ // since we have no other connected sessions, we can end discovery immediately
172
+ await this.#endIntentDiscovery(false);
173
+ return;
174
+ }
166
175
  // Listen out for discovery results directly sent to us
167
176
  await this.#mqttClient.subscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`);
168
177
  this.#discoveryTimeout = setTimeout(() => this.#endIntentDiscovery(), clampedTimeout);
@@ -173,10 +182,8 @@ class IntentController {
173
182
  throw new CloudInteropAPIError('Error starting intent discovery', 'ERR_STARTING_INTENT_DISCOVERY', error);
174
183
  }
175
184
  }
176
- async #endIntentDiscovery() {
185
+ async #endIntentDiscovery(mqttUnsubscribe = true) {
177
186
  if (this.#discovery.state !== 'in-progress') {
178
- // TODO: remove debug logs
179
- this.#logger('debug', 'Intent discovery not in progress');
180
187
  return;
181
188
  }
182
189
  if (this.#discoveryTimeout) {
@@ -186,10 +193,12 @@ class IntentController {
186
193
  this.#discovery.state = 'ended';
187
194
  // emit our aggregated events
188
195
  this.#events.emitEvent('aggregate-intent-details', { responses: this.#discovery.pendingIntentDetailsEvents });
189
- // gracefully end discovery
190
- await this.#mqttClient.unsubscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`).catch(() => {
191
- this.#logger('warn', `Error ending intent discovery: could not unsubscribe from discovery id ${this.#discovery.id}`);
192
- });
196
+ if (mqttUnsubscribe) {
197
+ // gracefully end discovery
198
+ await this.#mqttClient.unsubscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`).catch(() => {
199
+ this.#logger('warn', `Error ending intent discovery: could not unsubscribe from discovery id ${this.#discovery.id}`);
200
+ });
201
+ }
193
202
  await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/${this.#discovery.id}`, {
194
203
  method: 'DELETE',
195
204
  headers: getRequestHeaders(this.#connectionParams),
@@ -198,7 +207,6 @@ class IntentController {
198
207
  if (!deleteResponse.ok) {
199
208
  throw new Error(`Error ending intent discovery: ${deleteResponse.statusText}`);
200
209
  }
201
- this.#logger('debug', 'Intent discovery ended');
202
210
  })
203
211
  .catch((error) => {
204
212
  this.#logger('warn', `Error ending intent discovery: ${error}`);
@@ -242,6 +250,13 @@ class IntentController {
242
250
  return false;
243
251
  }
244
252
  async sendIntentResult(initiatingSessionId, result) {
253
+ if (!isErrorIntentResult(result)) {
254
+ // cloud-encode the source app id to support chained intent actions over cloud
255
+ // https://fdc3.finos.org/docs/2.0/api/spec#resolution-object -> "Use metadata about the resolving app instance to target a further intent"
256
+ const source = getSourceFromSession(this.#sessionDetails);
257
+ const encoded = encodeAppId(typeof result.source === 'string' ? result.source : result.source.appId, source);
258
+ result.source = typeof result.source === 'string' ? encoded : { ...result.source, appId: encoded };
259
+ }
245
260
  const { sessionId } = getSourceFromSession(this.#sessionDetails);
246
261
  const resultResponse = await fetch(`${this.#url}/api/intents/${initiatingSessionId}/result/${sessionId}`, {
247
262
  method: 'POST',
@@ -578,8 +593,13 @@ class CloudInteropAPI {
578
593
  if (contextEvent.source.sessionId === sessionDetails.sessionId) {
579
594
  return;
580
595
  }
581
- const { contextGroup, context, source, history } = contextEvent;
582
- this.#events.emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
596
+ const { context, payload, contextGroup, channelName, source, history } = contextEvent;
597
+ this.#events.emitEvent('context', {
598
+ contextGroup: channelName || contextGroup,
599
+ context: payload || context,
600
+ source,
601
+ history: { ...history, clientReceived: Date.now() },
602
+ });
583
603
  }
584
604
  else if (topic.startsWith(`${sessionDetails.sessionRootTopic}/commands`)) {
585
605
  this.#handleCommandMessage(messageEnvelope);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfin/cloud-interop-core-api",
3
- "version": "0.0.1-alpha.e8aa2c9",
3
+ "version": "0.0.1-alpha.e9e4a55",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "main": "./index.cjs",
@@ -13,6 +13,6 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "mqtt": "^5.3.1",
16
- "zod": "^3.24.1"
16
+ "zod": "^3.24.2"
17
17
  }
18
18
  }