@openfin/cloud-interop-core-api 0.0.1-alpha.6d7b62b → 0.0.1-alpha.6da3439

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.
@@ -1,12 +1,24 @@
1
1
  import mqtt from 'mqtt';
2
2
  import { z } from 'zod';
3
3
 
4
+ /**
5
+ * Raised when an interop client receives an aggregate of intents from other connected sessions
6
+ * @public
7
+ */
4
8
  export declare type AggregateIntentDetailsEvent = {
5
9
  responses: IntentDetailsEvent[];
6
10
  };
7
11
 
12
+ /**
13
+ * Represents an app intent
14
+ * @public
15
+ */
8
16
  export declare type AppIdentifier = z.infer<typeof appIdentifierSchema>;
9
17
 
18
+ /**
19
+ * Schema for an AppIdentifier
20
+ * @public
21
+ */
10
22
  declare const appIdentifierSchema: z.ZodObject<{
11
23
  appId: z.ZodString;
12
24
  instanceId: z.ZodOptional<z.ZodString>;
@@ -18,8 +30,16 @@ declare const appIdentifierSchema: z.ZodObject<{
18
30
  instanceId?: string | undefined;
19
31
  }>;
20
32
 
33
+ /**
34
+ * Represents an app intent
35
+ * @public
36
+ */
21
37
  export declare type AppIntent = z.infer<typeof appIntentSchema>;
22
38
 
39
+ /**
40
+ * Schema for an app intent
41
+ * @public
42
+ */
23
43
  declare const appIntentSchema: z.ZodObject<{
24
44
  intent: z.ZodObject<{
25
45
  displayName: z.ZodString;
@@ -175,30 +195,36 @@ export declare class AuthorizationError extends CloudInteropAPIError {
175
195
  }
176
196
 
177
197
  /**
178
- * @internal
179
198
  * Represents a base event for the cloud interop api
199
+ * @public
180
200
  * @property {Source} source - The source of the event
181
201
  * @property {HistoryRecord} history - A trail of timestamps of when the context hit various points in the system
182
202
  */
183
- declare type BaseCloudInteropAPIEvent = {
203
+ export declare type BaseCloudInteropAPIEvent = {
184
204
  /**
185
205
  * The source of the event
186
206
  */
187
207
  source: Source;
188
208
  /**
189
209
  * A trail of timestamps of when the event hit various points in the system
210
+ * @internal
190
211
  */
191
212
  history?: HistoryRecord;
192
213
  };
193
214
 
194
215
  /**
195
216
  * Represents an Intent Discovery Event
217
+ * @public
196
218
  */
197
- declare type BaseIntentDiscoveryEvent = BaseIntentEvent & {
219
+ export declare type BaseIntentDiscoveryEvent = BaseIntentEvent & {
198
220
  discoveryId: string;
199
221
  };
200
222
 
201
- declare type BaseIntentEvent = BaseCloudInteropAPIEvent & {
223
+ /**
224
+ * Represents a base intent event
225
+ * @public
226
+ */
227
+ export declare type BaseIntentEvent = BaseCloudInteropAPIEvent & {
202
228
  /**
203
229
  * The session id of the interop client that initiated an intent action
204
230
  */
@@ -208,9 +234,8 @@ declare type BaseIntentEvent = BaseCloudInteropAPIEvent & {
208
234
  /**
209
235
  * Represents a single connection to a Cloud Interop service
210
236
  *
211
- * @export
212
- * @class CloudInteropAPI
213
- * @implements {Client}
237
+ * @public
238
+ * @class
214
239
  */
215
240
  export declare class CloudInteropAPI {
216
241
  #private;
@@ -221,7 +246,7 @@ export declare class CloudInteropAPI {
221
246
  * Connects and creates a session on the Cloud Interop service
222
247
  *
223
248
  * @param {ConnectParameters} parameters - The parameters to use to connect
224
- * @return {*} {Promise<void>}
249
+ * @returns {*} {Promise<void>}
225
250
  * @memberof CloudInteropAPI
226
251
  * @throws {CloudInteropAPIError} - If an error occurs during connection
227
252
  * @throws {AuthorizationError} - If the connection is unauthorized
@@ -230,7 +255,7 @@ export declare class CloudInteropAPI {
230
255
  /**
231
256
  * Disconnects from the Cloud Interop service
232
257
  *
233
- * @return {*} {Promise<void>}
258
+ * @returns {*} {Promise<void>}
234
259
  * @memberof CloudInteropAPI
235
260
  * @throws {CloudInteropAPIError} - If an error occurs during disconnection
236
261
  */
@@ -240,14 +265,14 @@ export declare class CloudInteropAPI {
240
265
  *
241
266
  * @param {string} contextGroup - The context group to publish to
242
267
  * @param {object} context - The context to publish
243
- * @return {*} {Promise<void>}
268
+ * @returns {*} {Promise<void>}
244
269
  * @memberof CloudInteropAPI
245
270
  */
246
- setContext(contextGroup: string, context: object): Promise<void>;
271
+ setContext(contextGroup: string, context: InferredContext): Promise<void>;
247
272
  /**
248
273
  * Starts an intent discovery operation
249
274
  *
250
- * @return {*} {Promise<void>}
275
+ * @returns {*} {Promise<void>}
251
276
  * @memberof CloudInteropAPI
252
277
  * @throws {CloudInteropAPIError} - If an error occurs during intent discovery
253
278
  */
@@ -256,6 +281,7 @@ export declare class CloudInteropAPI {
256
281
  reportAppIntents(discoveryId: string, intents: AppIntent[]): Promise<boolean>;
257
282
  sendIntentResult(initiatingSessionId: string, result: IntentResult): Promise<void>;
258
283
  parseSessionId(appId: AppIdentifier | string): string;
284
+ parseAppId(appId: AppIdentifier | string): string;
259
285
  addEventListener<K extends keyof EventMap>(type: K, callback: EventMap[K]): void;
260
286
  removeEventListener<K extends keyof EventMap>(type: K, callback: EventMap[K]): void;
261
287
  once<K extends keyof EventMap>(type: K, callback: EventMap[K]): void;
@@ -275,6 +301,9 @@ export declare type CloudInteropSettings = {
275
301
  url: string;
276
302
  };
277
303
 
304
+ /**
305
+ * @internal
306
+ */
278
307
  export declare type CommandMessage = ReportIntentsCommand | IntentDetailsCommand | RaiseIntentCommand | IntentResultCommand;
279
308
 
280
309
  /**
@@ -347,9 +376,7 @@ export declare type ConnectParameters = {
347
376
 
348
377
  /**
349
378
  * Represents a context received from another cloud interop publisher
350
- *
351
- * @export
352
- * @type ContextEvent
379
+ * @public
353
380
  * @property {string} contextGroup - The context group
354
381
  * @property {object} context - The context
355
382
  */
@@ -364,6 +391,10 @@ export declare type ContextEvent = BaseCloudInteropAPIEvent & {
364
391
  context?: InferredContext;
365
392
  };
366
393
 
394
+ /**
395
+ * Schema for context object
396
+ * @public
397
+ */
367
398
  declare const contextSchema: z.ZodIntersection<z.ZodObject<{
368
399
  type: z.ZodString;
369
400
  name: z.ZodOptional<z.ZodString>;
@@ -389,6 +420,14 @@ export declare type CreateSessionResponse = {
389
420
  sourceId: string;
390
421
  };
391
422
 
423
+ declare const errorSchema: z.ZodObject<{
424
+ error: z.ZodString;
425
+ }, "strip", z.ZodTypeAny, {
426
+ error: string;
427
+ }, {
428
+ error: string;
429
+ }>;
430
+
392
431
  export declare type EventListenersMap = Map<keyof EventMap, Array<(...args: Parameters<EventMap[keyof EventMap]>) => void>>;
393
432
 
394
433
  export declare type EventMap = {
@@ -407,9 +446,14 @@ export declare type EventMap = {
407
446
 
408
447
  /**
409
448
  * Options for finding intents by intent name or context
449
+ * @public
410
450
  */
411
451
  export declare type FindIntentOptions = z.infer<typeof findIntentOptionsSchema>;
412
452
 
453
+ /**
454
+ * Schema for findOptions for the StartDiscovery operation
455
+ * @public
456
+ */
413
457
  declare const findIntentOptionsSchema: z.ZodUnion<[z.ZodObject<{
414
458
  type: z.ZodLiteral<"find-intent">;
415
459
  intent: z.ZodString;
@@ -480,8 +524,8 @@ declare const findIntentOptionsSchema: z.ZodUnion<[z.ZodObject<{
480
524
  }>]>;
481
525
 
482
526
  /**
483
- * @internal
484
527
  * A trace record for performance monitoring
528
+ * @internal
485
529
  */
486
530
  declare type HistoryRecord = {
487
531
  /**
@@ -503,19 +547,33 @@ declare type HistoryRecord = {
503
547
  clientReceived?: number;
504
548
  };
505
549
 
550
+ /**
551
+ * Represents a context object
552
+ * @public
553
+ */
506
554
  export declare type InferredContext = z.infer<typeof contextSchema>;
507
555
 
556
+ export declare type InferredError = z.infer<typeof errorSchema>;
557
+
558
+ /**
559
+ * @internal
560
+ */
508
561
  export declare type IntentDetailsCommand = IntentDetailsEvent & {
509
562
  command: 'intent-details';
510
563
  };
511
564
 
512
565
  /**
513
566
  * Raised when an interop client receives intents from other connected sessions
567
+ * @public
514
568
  */
515
569
  export declare type IntentDetailsEvent = BaseIntentDiscoveryEvent & {
516
570
  intents: AppIntent[];
517
571
  };
518
572
 
573
+ /**
574
+ * Represents an intent resolution
575
+ * @public
576
+ */
519
577
  export declare type IntentResolution = z.infer<typeof intentResolutionSchema>;
520
578
 
521
579
  declare const intentResolutionSchema: z.ZodObject<{
@@ -570,19 +628,31 @@ declare const intentResolutionSchema: z.ZodObject<{
570
628
  } & Record<string, any>) | undefined;
571
629
  }>;
572
630
 
631
+ /**
632
+ * Represents an intent result
633
+ * @public
634
+ */
573
635
  export declare type IntentResult = z.infer<typeof intentResultSchema>;
574
636
 
637
+ /**
638
+ * @internal
639
+ */
575
640
  export declare type IntentResultCommand = IntentResultEvent & {
576
641
  command: 'intent-result';
577
642
  };
578
643
 
579
644
  /**
580
645
  * Raised when an interop client receives an intent result after raising an intent on a target session
646
+ * @public
581
647
  */
582
648
  export declare type IntentResultEvent = BaseIntentEvent & {
583
649
  result: IntentResult;
584
650
  };
585
651
 
652
+ /**
653
+ * Schema for the result of an intent
654
+ * @public
655
+ */
586
656
  declare const intentResultSchema: z.ZodUnion<[z.ZodObject<{
587
657
  error: z.ZodString;
588
658
  }, "strip", z.ZodTypeAny, {
@@ -645,18 +715,23 @@ export declare type LogLevel = 'log' | 'debug' | 'info' | 'warn' | 'error';
645
715
 
646
716
  /**
647
717
  * Options used to raise an Intent
718
+ * @public
648
719
  */
649
720
  export declare type RaiseIntentAPIOptions = {
650
721
  raiseOptions: RaiseIntentOptions;
651
722
  appId: AppIdentifier | string;
652
723
  };
653
724
 
725
+ /**
726
+ * @internal
727
+ */
654
728
  export declare type RaiseIntentCommand = RaiseIntentEvent & {
655
729
  command: 'raise-intent';
656
730
  };
657
731
 
658
732
  /**
659
733
  * Raised when an interop client invokes an intent
734
+ * @public
660
735
  */
661
736
  export declare type RaiseIntentEvent = BaseIntentEvent & {
662
737
  targetSessionId: string;
@@ -665,9 +740,14 @@ export declare type RaiseIntentEvent = BaseIntentEvent & {
665
740
 
666
741
  /**
667
742
  * Options for raising an intent by intent name or context
743
+ * @public
668
744
  */
669
745
  export declare type RaiseIntentOptions = z.infer<typeof raiseIntentOptionsSchema>;
670
746
 
747
+ /**
748
+ * Schema for raiseOptions for the RaiseIntent operation
749
+ * @public
750
+ */
671
751
  declare const raiseIntentOptionsSchema: z.ZodUnion<[z.ZodObject<{
672
752
  type: z.ZodLiteral<"raise-intent">;
673
753
  intent: z.ZodString;
@@ -731,12 +811,16 @@ declare const raiseIntentOptionsSchema: z.ZodUnion<[z.ZodObject<{
731
811
  } & Record<string, any>;
732
812
  }>]>;
733
813
 
814
+ /**
815
+ * @internal
816
+ */
734
817
  export declare type ReportIntentsCommand = ReportIntentsEvent & {
735
818
  command: 'report-intents';
736
819
  };
737
820
 
738
821
  /**
739
822
  * Raised when an interop client requests intents from other connected sessions.
823
+ * @public
740
824
  */
741
825
  export declare type ReportIntentsEvent = BaseIntentDiscoveryEvent & {
742
826
  /**
@@ -754,6 +838,7 @@ export declare type ReportIntentsEvent = BaseIntentDiscoveryEvent & {
754
838
 
755
839
  /**
756
840
  * Represents a source session
841
+ * @public
757
842
  */
758
843
  export declare type Source = {
759
844
  /**
@@ -780,6 +865,7 @@ export declare type Source = {
780
865
 
781
866
  /**
782
867
  * Options used to start an Intent Discovery Operation, with possible constraints for the responses
868
+ * @public
783
869
  */
784
870
  export declare type StartIntentDiscoveryOptions = {
785
871
  findOptions: FindIntentOptions;
@@ -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,19 +85,35 @@ 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
- * Decodes the app id to extract the sessionId, returns '' if not able to parse
99
- * @param app
100
- * @returns
102
+ * Decodes the AppIdentifier to extract the appId, sourceId, and sessionId.
103
+ * @returns an object with:
104
+ * - appId: The appId, or the original appId if unable to parse.
105
+ * - sourceId: The sourceId, or an '' if unable to parse.
106
+ * - sessionId: The sessionId, or an '' if unable to parse.
101
107
  */
102
- const parseSessionId = (appId) => (typeof appId === 'string' ? appId : appId.appId).split(APP_ID_DELIM)?.[2]?.trim() ?? '';
108
+ const parseCloudAppId = (appId = '') => {
109
+ const originalAppString = typeof appId === 'string' ? appId : (appId.appId ?? '');
110
+ const parts = originalAppString.split(APP_ID_DELIM);
111
+ return {
112
+ appId: parts[0]?.trim() ?? originalAppString,
113
+ sourceId: parts[1]?.trim() ?? '',
114
+ sessionId: parts[2]?.trim() ?? '',
115
+ };
116
+ };
103
117
  const getSourceFromSession = (sessionDetails) => ({
104
118
  sessionId: sessionDetails.sessionId,
105
119
  sourceId: sessionDetails.sourceId,
@@ -148,13 +162,18 @@ class IntentController {
148
162
  body: JSON.stringify({ findOptions }),
149
163
  });
150
164
  if (!startResponse.ok) {
151
- throw new Error(startResponse.statusText);
165
+ throw new Error(`Error creating intent discovery record: ${startResponse.statusText}`);
152
166
  }
153
167
  // TODO: type this response?
154
168
  const json = await startResponse.json();
155
169
  this.#discovery.id = json.discoveryId;
156
170
  this.#discovery.sessionCount = json.sessionCount;
157
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
+ }
158
177
  // Listen out for discovery results directly sent to us
159
178
  await this.#mqttClient.subscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`);
160
179
  this.#discoveryTimeout = setTimeout(() => this.#endIntentDiscovery(), clampedTimeout);
@@ -165,10 +184,8 @@ class IntentController {
165
184
  throw new CloudInteropAPIError('Error starting intent discovery', 'ERR_STARTING_INTENT_DISCOVERY', error);
166
185
  }
167
186
  }
168
- async #endIntentDiscovery() {
187
+ async #endIntentDiscovery(mqttUnsubscribe = true) {
169
188
  if (this.#discovery.state !== 'in-progress') {
170
- // TODO: remove debug logs
171
- this.#logger('debug', 'Intent discovery not in progress');
172
189
  return;
173
190
  }
174
191
  if (this.#discoveryTimeout) {
@@ -178,10 +195,12 @@ class IntentController {
178
195
  this.#discovery.state = 'ended';
179
196
  // emit our aggregated events
180
197
  this.#events.emitEvent('aggregate-intent-details', { responses: this.#discovery.pendingIntentDetailsEvents });
181
- // gracefully end discovery
182
- await this.#mqttClient.unsubscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`).catch(() => {
183
- this.#logger('warn', `Error ending intent discovery: could not unsubscribe from discovery id ${this.#discovery.id}`);
184
- });
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
+ }
185
204
  await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/${this.#discovery.id}`, {
186
205
  method: 'DELETE',
187
206
  headers: getRequestHeaders(this.#connectionParams),
@@ -190,7 +209,6 @@ class IntentController {
190
209
  if (!deleteResponse.ok) {
191
210
  throw new Error(`Error ending intent discovery: ${deleteResponse.statusText}`);
192
211
  }
193
- this.#logger('debug', 'Intent discovery ended');
194
212
  })
195
213
  .catch((error) => {
196
214
  this.#logger('warn', `Error ending intent discovery: ${error}`);
@@ -199,7 +217,7 @@ class IntentController {
199
217
  this.#discovery = newDiscovery();
200
218
  }
201
219
  async raiseIntent({ raiseOptions, appId }) {
202
- const targetSessionId = parseSessionId(appId);
220
+ const targetSessionId = parseCloudAppId(appId).sessionId;
203
221
  if (!targetSessionId) {
204
222
  // TODO: should we add more info here about the format?
205
223
  throw new CloudInteropAPIError(`Invalid AppId specified, must be encoded as a cloud-session app id`, 'ERR_INVALID_TARGET_SESSION_ID');
@@ -234,6 +252,13 @@ class IntentController {
234
252
  return false;
235
253
  }
236
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
+ }
237
262
  const { sessionId } = getSourceFromSession(this.#sessionDetails);
238
263
  const resultResponse = await fetch(`${this.#url}/api/intents/${initiatingSessionId}/result/${sessionId}`, {
239
264
  method: 'POST',
@@ -311,9 +336,8 @@ const BadUserNamePasswordError = 134;
311
336
  /**
312
337
  * Represents a single connection to a Cloud Interop service
313
338
  *
314
- * @export
315
- * @class CloudInteropAPI
316
- * @implements {Client}
339
+ * @public
340
+ * @class
317
341
  */
318
342
  class CloudInteropAPI {
319
343
  #cloudInteropSettings;
@@ -342,7 +366,7 @@ class CloudInteropAPI {
342
366
  * Connects and creates a session on the Cloud Interop service
343
367
  *
344
368
  * @param {ConnectParameters} parameters - The parameters to use to connect
345
- * @return {*} {Promise<void>}
369
+ * @returns {*} {Promise<void>}
346
370
  * @memberof CloudInteropAPI
347
371
  * @throws {CloudInteropAPIError} - If an error occurs during connection
348
372
  * @throws {AuthorizationError} - If the connection is unauthorized
@@ -454,7 +478,7 @@ class CloudInteropAPI {
454
478
  /**
455
479
  * Disconnects from the Cloud Interop service
456
480
  *
457
- * @return {*} {Promise<void>}
481
+ * @returns {*} {Promise<void>}
458
482
  * @memberof CloudInteropAPI
459
483
  * @throws {CloudInteropAPIError} - If an error occurs during disconnection
460
484
  */
@@ -466,7 +490,7 @@ class CloudInteropAPI {
466
490
  *
467
491
  * @param {string} contextGroup - The context group to publish to
468
492
  * @param {object} context - The context to publish
469
- * @return {*} {Promise<void>}
493
+ * @returns {*} {Promise<void>}
470
494
  * @memberof CloudInteropAPI
471
495
  */
472
496
  async setContext(contextGroup, context) {
@@ -493,7 +517,7 @@ class CloudInteropAPI {
493
517
  /**
494
518
  * Starts an intent discovery operation
495
519
  *
496
- * @return {*} {Promise<void>}
520
+ * @returns {*} {Promise<void>}
497
521
  * @memberof CloudInteropAPI
498
522
  * @throws {CloudInteropAPIError} - If an error occurs during intent discovery
499
523
  */
@@ -514,7 +538,10 @@ class CloudInteropAPI {
514
538
  return this.#intents?.sendIntentResult(initiatingSessionId, result);
515
539
  }
516
540
  parseSessionId(appId) {
517
- return parseSessionId(appId);
541
+ return parseCloudAppId(appId).sessionId;
542
+ }
543
+ parseAppId(appId) {
544
+ return parseCloudAppId(appId).appId;
518
545
  }
519
546
  addEventListener(type, callback) {
520
547
  this.#events.addEventListener(type, callback);
@@ -526,10 +553,13 @@ class CloudInteropAPI {
526
553
  this.#events.once(type, callback);
527
554
  }
528
555
  async #disconnect(fireDisconnectedEvent) {
529
- if (!this.#sessionDetails || !this.#connectionParams) {
556
+ if (!this.#sessionDetails) {
530
557
  return;
531
558
  }
532
559
  try {
560
+ if (!this.#connectionParams) {
561
+ throw new Error('Connect parameters must be provided');
562
+ }
533
563
  const disconnectResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
534
564
  method: 'DELETE',
535
565
  headers: getRequestHeaders(this.#connectionParams),
@@ -565,8 +595,13 @@ class CloudInteropAPI {
565
595
  if (contextEvent.source.sessionId === sessionDetails.sessionId) {
566
596
  return;
567
597
  }
568
- const { contextGroup, context, source, history } = contextEvent;
569
- 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
+ });
570
605
  }
571
606
  else if (topic.startsWith(`${sessionDetails.sessionRootTopic}/commands`)) {
572
607
  this.#handleCommandMessage(messageEnvelope);
@@ -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,19 +83,35 @@ 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
- * Decodes the app id to extract the sessionId, returns '' if not able to parse
97
- * @param app
98
- * @returns
100
+ * Decodes the AppIdentifier to extract the appId, sourceId, and sessionId.
101
+ * @returns an object with:
102
+ * - appId: The appId, or the original appId if unable to parse.
103
+ * - sourceId: The sourceId, or an '' if unable to parse.
104
+ * - sessionId: The sessionId, or an '' if unable to parse.
99
105
  */
100
- const parseSessionId = (appId) => (typeof appId === 'string' ? appId : appId.appId).split(APP_ID_DELIM)?.[2]?.trim() ?? '';
106
+ const parseCloudAppId = (appId = '') => {
107
+ const originalAppString = typeof appId === 'string' ? appId : (appId.appId ?? '');
108
+ const parts = originalAppString.split(APP_ID_DELIM);
109
+ return {
110
+ appId: parts[0]?.trim() ?? originalAppString,
111
+ sourceId: parts[1]?.trim() ?? '',
112
+ sessionId: parts[2]?.trim() ?? '',
113
+ };
114
+ };
101
115
  const getSourceFromSession = (sessionDetails) => ({
102
116
  sessionId: sessionDetails.sessionId,
103
117
  sourceId: sessionDetails.sourceId,
@@ -146,13 +160,18 @@ class IntentController {
146
160
  body: JSON.stringify({ findOptions }),
147
161
  });
148
162
  if (!startResponse.ok) {
149
- throw new Error(startResponse.statusText);
163
+ throw new Error(`Error creating intent discovery record: ${startResponse.statusText}`);
150
164
  }
151
165
  // TODO: type this response?
152
166
  const json = await startResponse.json();
153
167
  this.#discovery.id = json.discoveryId;
154
168
  this.#discovery.sessionCount = json.sessionCount;
155
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
+ }
156
175
  // Listen out for discovery results directly sent to us
157
176
  await this.#mqttClient.subscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`);
158
177
  this.#discoveryTimeout = setTimeout(() => this.#endIntentDiscovery(), clampedTimeout);
@@ -163,10 +182,8 @@ class IntentController {
163
182
  throw new CloudInteropAPIError('Error starting intent discovery', 'ERR_STARTING_INTENT_DISCOVERY', error);
164
183
  }
165
184
  }
166
- async #endIntentDiscovery() {
185
+ async #endIntentDiscovery(mqttUnsubscribe = true) {
167
186
  if (this.#discovery.state !== 'in-progress') {
168
- // TODO: remove debug logs
169
- this.#logger('debug', 'Intent discovery not in progress');
170
187
  return;
171
188
  }
172
189
  if (this.#discoveryTimeout) {
@@ -176,10 +193,12 @@ class IntentController {
176
193
  this.#discovery.state = 'ended';
177
194
  // emit our aggregated events
178
195
  this.#events.emitEvent('aggregate-intent-details', { responses: this.#discovery.pendingIntentDetailsEvents });
179
- // gracefully end discovery
180
- await this.#mqttClient.unsubscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`).catch(() => {
181
- this.#logger('warn', `Error ending intent discovery: could not unsubscribe from discovery id ${this.#discovery.id}`);
182
- });
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
+ }
183
202
  await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/${this.#discovery.id}`, {
184
203
  method: 'DELETE',
185
204
  headers: getRequestHeaders(this.#connectionParams),
@@ -188,7 +207,6 @@ class IntentController {
188
207
  if (!deleteResponse.ok) {
189
208
  throw new Error(`Error ending intent discovery: ${deleteResponse.statusText}`);
190
209
  }
191
- this.#logger('debug', 'Intent discovery ended');
192
210
  })
193
211
  .catch((error) => {
194
212
  this.#logger('warn', `Error ending intent discovery: ${error}`);
@@ -197,7 +215,7 @@ class IntentController {
197
215
  this.#discovery = newDiscovery();
198
216
  }
199
217
  async raiseIntent({ raiseOptions, appId }) {
200
- const targetSessionId = parseSessionId(appId);
218
+ const targetSessionId = parseCloudAppId(appId).sessionId;
201
219
  if (!targetSessionId) {
202
220
  // TODO: should we add more info here about the format?
203
221
  throw new CloudInteropAPIError(`Invalid AppId specified, must be encoded as a cloud-session app id`, 'ERR_INVALID_TARGET_SESSION_ID');
@@ -232,6 +250,13 @@ class IntentController {
232
250
  return false;
233
251
  }
234
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
+ }
235
260
  const { sessionId } = getSourceFromSession(this.#sessionDetails);
236
261
  const resultResponse = await fetch(`${this.#url}/api/intents/${initiatingSessionId}/result/${sessionId}`, {
237
262
  method: 'POST',
@@ -309,9 +334,8 @@ const BadUserNamePasswordError = 134;
309
334
  /**
310
335
  * Represents a single connection to a Cloud Interop service
311
336
  *
312
- * @export
313
- * @class CloudInteropAPI
314
- * @implements {Client}
337
+ * @public
338
+ * @class
315
339
  */
316
340
  class CloudInteropAPI {
317
341
  #cloudInteropSettings;
@@ -340,7 +364,7 @@ class CloudInteropAPI {
340
364
  * Connects and creates a session on the Cloud Interop service
341
365
  *
342
366
  * @param {ConnectParameters} parameters - The parameters to use to connect
343
- * @return {*} {Promise<void>}
367
+ * @returns {*} {Promise<void>}
344
368
  * @memberof CloudInteropAPI
345
369
  * @throws {CloudInteropAPIError} - If an error occurs during connection
346
370
  * @throws {AuthorizationError} - If the connection is unauthorized
@@ -452,7 +476,7 @@ class CloudInteropAPI {
452
476
  /**
453
477
  * Disconnects from the Cloud Interop service
454
478
  *
455
- * @return {*} {Promise<void>}
479
+ * @returns {*} {Promise<void>}
456
480
  * @memberof CloudInteropAPI
457
481
  * @throws {CloudInteropAPIError} - If an error occurs during disconnection
458
482
  */
@@ -464,7 +488,7 @@ class CloudInteropAPI {
464
488
  *
465
489
  * @param {string} contextGroup - The context group to publish to
466
490
  * @param {object} context - The context to publish
467
- * @return {*} {Promise<void>}
491
+ * @returns {*} {Promise<void>}
468
492
  * @memberof CloudInteropAPI
469
493
  */
470
494
  async setContext(contextGroup, context) {
@@ -491,7 +515,7 @@ class CloudInteropAPI {
491
515
  /**
492
516
  * Starts an intent discovery operation
493
517
  *
494
- * @return {*} {Promise<void>}
518
+ * @returns {*} {Promise<void>}
495
519
  * @memberof CloudInteropAPI
496
520
  * @throws {CloudInteropAPIError} - If an error occurs during intent discovery
497
521
  */
@@ -512,7 +536,10 @@ class CloudInteropAPI {
512
536
  return this.#intents?.sendIntentResult(initiatingSessionId, result);
513
537
  }
514
538
  parseSessionId(appId) {
515
- return parseSessionId(appId);
539
+ return parseCloudAppId(appId).sessionId;
540
+ }
541
+ parseAppId(appId) {
542
+ return parseCloudAppId(appId).appId;
516
543
  }
517
544
  addEventListener(type, callback) {
518
545
  this.#events.addEventListener(type, callback);
@@ -524,10 +551,13 @@ class CloudInteropAPI {
524
551
  this.#events.once(type, callback);
525
552
  }
526
553
  async #disconnect(fireDisconnectedEvent) {
527
- if (!this.#sessionDetails || !this.#connectionParams) {
554
+ if (!this.#sessionDetails) {
528
555
  return;
529
556
  }
530
557
  try {
558
+ if (!this.#connectionParams) {
559
+ throw new Error('Connect parameters must be provided');
560
+ }
531
561
  const disconnectResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
532
562
  method: 'DELETE',
533
563
  headers: getRequestHeaders(this.#connectionParams),
@@ -563,8 +593,13 @@ class CloudInteropAPI {
563
593
  if (contextEvent.source.sessionId === sessionDetails.sessionId) {
564
594
  return;
565
595
  }
566
- const { contextGroup, context, source, history } = contextEvent;
567
- 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
+ });
568
603
  }
569
604
  else if (topic.startsWith(`${sessionDetails.sessionRootTopic}/commands`)) {
570
605
  this.#handleCommandMessage(messageEnvelope);
package/package.json CHANGED
@@ -1,241 +1,18 @@
1
1
  {
2
- "name": "@openfin/cloud-interop-core-api",
3
- "version": "0.0.1-alpha.6d7b62b",
4
- "type": "module",
5
- "description": "",
6
- "files": [
7
- "./dist/*"
8
- ],
9
- "main": "./dist/index.cjs",
10
- "browser": "./dist/index.mjs",
11
- "types": "./dist/bundle.d.ts",
12
- "scripts": {
13
- "build": "rimraf dist && tsx scripts/build.ts",
14
- "build:watch": "NODE_ENV=development npx nodemon --watch ./src -e .ts -x \"npm run build || exit 1\"",
15
- "typecheck": "tsc --noEmit",
16
- "test": "jest --coverage",
17
- "lint": "eslint . --max-warnings 0",
18
- "lint:fix": "eslint . --fix --max-warnings 0",
19
- "pack": "pushd dist && npm pack && popd"
20
- },
21
- "author": "",
22
- "license": "SEE LICENSE IN LICENSE.md",
23
- "devDependencies": {
24
- "@microsoft/api-extractor": "^7.49.1",
25
- "@rollup/plugin-commonjs": "^28.0.1",
26
- "@rollup/plugin-inject": "^5.0.5",
27
- "@rollup/plugin-node-resolve": "^15.2.3",
28
- "@rollup/plugin-typescript": "^11.1.6",
29
- "@types/jest": "^29.5.14",
30
- "@types/node": "^22.7.7",
31
- "@typescript-eslint/eslint-plugin": "^7.5.0",
32
- "@typescript-eslint/parser": "^7.15.0",
33
- "dotenv-defaults": "^5.0.2",
34
- "eslint": "^8.57.0",
35
- "eslint-config-prettier": "^9.1.0",
36
- "eslint-plugin-check-file": "^2.8.0",
37
- "eslint-plugin-prettier": "^5.2.1",
38
- "eslint-plugin-simple-import-sort": "^12.1.1",
39
- "eslint-plugin-unicorn": "^55.0.0",
40
- "eslint-plugin-unused-imports": "^4.1.4",
41
- "jest": "^29.7.0",
42
- "nodemon": "^3.1.9",
43
- "prettier": "^3.3.3",
44
- "rollup": "^4.30.1",
45
- "ts-jest": "^29.2.5",
46
- "tsx": "^4.19.2",
47
- "typescript": "^5.7.3"
48
- },
49
- "optionalDependencies": {
50
- "@rollup/rollup-linux-x64-gnu": "*"
51
- },
52
- "dependencies": {
53
- "@openfin/shared-utils": "file:../shared-utils",
54
- "mqtt": "^5.3.1",
55
- "zod": "^3.24.1"
56
- },
57
- "eslintConfig": {
58
- "env": {
59
- "browser": true,
60
- "node": true
61
- },
62
- "parser": "@typescript-eslint/parser",
63
- "parserOptions": {
64
- "ecmaVersion": "latest",
65
- "project": true,
66
- "sourceType": "module"
67
- },
68
- "extends": [
69
- "eslint:recommended",
70
- "plugin:@typescript-eslint/recommended",
71
- "plugin:@typescript-eslint/strict",
72
- "plugin:unicorn/recommended",
73
- "plugin:prettier/recommended"
74
- ],
75
- "plugins": [
76
- "prettier",
77
- "check-file",
78
- "simple-import-sort",
79
- "unused-imports"
80
- ],
81
- "rules": {
82
- "unused-imports/no-unused-imports": "warn",
83
- "unicorn/prevent-abbreviations": [
84
- "error",
85
- {
86
- "replacements": {
87
- "props": false,
88
- "prop": false,
89
- "ref": false,
90
- "args": false,
91
- "arg": false,
92
- "src": false,
93
- "dev": false,
94
- "str": false,
95
- "req": false,
96
- "res": false
97
- }
98
- }
99
- ],
100
- "@typescript-eslint/no-non-null-assertion": "error",
101
- "unicorn/no-nested-ternary": "off",
102
- "unicorn/no-array-for-each": "off",
103
- "unicorn/no-useless-undefined": "off",
104
- "unicorn/no-null": "off",
105
- "eqeqeq": [
106
- "error",
107
- "always"
108
- ],
109
- "no-alert": "error",
110
- "no-eval": "error",
111
- "prettier/prettier": "warn",
112
- "simple-import-sort/imports": [
113
- "error",
114
- {
115
- "groups": [
116
- [
117
- "^react$"
118
- ],
119
- [
120
- "^react"
121
- ],
122
- [
123
- "^next"
124
- ],
125
- [
126
- "^zod"
127
- ],
128
- [
129
- "^@radix-ui/"
130
- ],
131
- [
132
- "^[^.]"
133
- ],
134
- [
135
- "@/components/ui/.*"
136
- ],
137
- [
138
- "@/components/.*"
139
- ],
140
- [
141
- "@/config/.*"
142
- ],
143
- [
144
- "@/lib/.*"
145
- ],
146
- [
147
- "^\\.\\.(?!/?$)",
148
- "^\\.\\./?$"
149
- ],
150
- [
151
- "^\\./(?=.*/)(?!/?$)",
152
- "^\\.(?!/?$)",
153
- "^\\./?$"
154
- ],
155
- [
156
- "^.+\\.s?css$"
157
- ]
158
- ]
159
- }
160
- ],
161
- "check-file/no-index": "off",
162
- "check-file/filename-naming-convention": [
163
- "error",
164
- {
165
- "**/*.{jsx,tsx}": "KEBAB_CASE",
166
- "**/*.{js,ts}": "KEBAB_CASE"
167
- },
168
- {
169
- "ignoreMiddleExtensions": true
170
- }
171
- ],
172
- "check-file/filename-blocklist": [
173
- "error",
174
- {
175
- "**/*.spec.js": "*.test.js",
176
- "**/*.spec.jsx": "*.test.jsx",
177
- "**/*.spec.ts": "*.test.ts",
178
- "**/*.spec.tsx": "*.test.tsx"
179
- }
180
- ],
181
- "no-restricted-syntax": [
182
- "error",
183
- {
184
- "selector": "TSEnumDeclaration",
185
- "message": "Prefer string unions to enums."
186
- }
187
- ],
188
- "curly": [
189
- "error",
190
- "multi-line"
191
- ],
192
- "@typescript-eslint/consistent-type-definitions": [
193
- "error",
194
- "type"
195
- ],
196
- "@typescript-eslint/no-unused-vars": [
197
- "warn",
198
- {
199
- "argsIgnorePattern": "^_",
200
- "varsIgnorePattern": "^_"
201
- }
202
- ],
203
- "@typescript-eslint/no-explicit-any": "warn"
204
- },
205
- "ignorePatterns": [
206
- "node_modules",
207
- "out",
208
- "build",
209
- "scripts",
210
- "dist",
211
- "coverage",
212
- "tests",
213
- "rollup.config.mjs",
214
- "examples"
215
- ]
216
- },
217
- "jest": {
218
- "collectCoverage": true,
219
- "collectCoverageFrom": [
220
- "src/**/*.ts"
221
- ],
222
- "coverageReporters": [
223
- "lcov",
224
- "text-summary"
225
- ],
226
- "preset": "ts-jest",
227
- "restoreMocks": true,
228
- "setupFiles": [],
229
- "testMatch": [
230
- "**/tests/*.test.ts"
231
- ],
232
- "testTimeout": 100000
233
- },
234
- "prettier": {
235
- "printWidth": 160,
236
- "semi": true,
237
- "singleQuote": true,
238
- "tabWidth": 4,
239
- "trailingComma": "all"
240
- }
2
+ "name": "@openfin/cloud-interop-core-api",
3
+ "version": "0.0.1-alpha.6da3439",
4
+ "type": "module",
5
+ "description": "",
6
+ "main": "./index.cjs",
7
+ "browser": "./index.mjs",
8
+ "types": "./bundle.d.ts",
9
+ "author": "",
10
+ "license": "SEE LICENSE IN LICENSE.md",
11
+ "optionalDependencies": {
12
+ "@rollup/rollup-linux-x64-gnu": "*"
13
+ },
14
+ "dependencies": {
15
+ "mqtt": "^5.3.1",
16
+ "zod": "^3.24.2"
17
+ }
241
18
  }
package/dist/LICENSE.md DELETED
@@ -1,4 +0,0 @@
1
- Learn more about OpenFin licensing at the links listed below or email us at support@openfin.co with questions.
2
-
3
- - [Developer agreement](https://openfin.co/developer-agreement)
4
-
package/dist/README.md DELETED
@@ -1,14 +0,0 @@
1
- # @openfin/cloud-interop-core-api
2
-
3
- This package contains the core interop library that handles all interactions with the Here™ Cloud Interop Service.
4
-
5
- It is callable via browser or node applications.
6
-
7
-
8
- ## Authentication
9
-
10
- The library supports authentication with the Here™ Cloud Interop Service using the following methods:
11
- - Basic Authentication
12
- - JWT Token Authentication
13
- - Default Authentication i.e. Interactive session based authentication using cookies
14
-
package/dist/package.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "name": "@openfin/cloud-interop-core-api",
3
- "version": "0.0.1-alpha",
4
- "type": "module",
5
- "description": "",
6
- "main": "./index.cjs",
7
- "browser": "./index.mjs",
8
- "types": "./bundle.d.ts",
9
- "author": "",
10
- "license": "SEE LICENSE IN LICENSE.md",
11
- "optionalDependencies": {
12
- "@rollup/rollup-linux-x64-gnu": "*"
13
- },
14
- "dependencies": {
15
- "mqtt": "^5.3.1",
16
- "zod": "^3.24.1"
17
- }
18
- }