@equinor/fusion-framework-module-context 0.0.0-context-error-20240131144633

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 (61) hide show
  1. package/CHANGELOG.md +522 -0
  2. package/LICENSE +21 -0
  3. package/dist/esm/ContextConfigBuilder.js +79 -0
  4. package/dist/esm/ContextConfigBuilder.js.map +1 -0
  5. package/dist/esm/ContextProvider.js +279 -0
  6. package/dist/esm/ContextProvider.js.map +1 -0
  7. package/dist/esm/client/ContextClient.js +62 -0
  8. package/dist/esm/client/ContextClient.js.map +1 -0
  9. package/dist/esm/configurator.js +86 -0
  10. package/dist/esm/configurator.js.map +1 -0
  11. package/dist/esm/errors.js +26 -0
  12. package/dist/esm/errors.js.map +1 -0
  13. package/dist/esm/index.js +6 -0
  14. package/dist/esm/index.js.map +1 -0
  15. package/dist/esm/module.js +55 -0
  16. package/dist/esm/module.js.map +1 -0
  17. package/dist/esm/selectors.js +44 -0
  18. package/dist/esm/selectors.js.map +1 -0
  19. package/dist/esm/types.js +2 -0
  20. package/dist/esm/types.js.map +1 -0
  21. package/dist/esm/utils/enable-context.js +10 -0
  22. package/dist/esm/utils/enable-context.js.map +1 -0
  23. package/dist/esm/utils/index.js +3 -0
  24. package/dist/esm/utils/index.js.map +1 -0
  25. package/dist/esm/utils/resolve-context-from-path.js +17 -0
  26. package/dist/esm/utils/resolve-context-from-path.js.map +1 -0
  27. package/dist/esm/utils/resolve-initial-context.js +16 -0
  28. package/dist/esm/utils/resolve-initial-context.js.map +1 -0
  29. package/dist/esm/version.js +2 -0
  30. package/dist/esm/version.js.map +1 -0
  31. package/dist/tsconfig.tsbuildinfo +1 -0
  32. package/dist/types/ContextConfigBuilder.d.ts +25 -0
  33. package/dist/types/ContextProvider.d.ts +99 -0
  34. package/dist/types/client/ContextClient.d.ts +22 -0
  35. package/dist/types/configurator.d.ts +41 -0
  36. package/dist/types/errors.d.ts +8 -0
  37. package/dist/types/index.d.ts +5 -0
  38. package/dist/types/module.d.ts +20 -0
  39. package/dist/types/selectors.d.ts +4 -0
  40. package/dist/types/types.d.ts +34 -0
  41. package/dist/types/utils/enable-context.d.ts +4 -0
  42. package/dist/types/utils/index.d.ts +2 -0
  43. package/dist/types/utils/resolve-context-from-path.d.ts +8 -0
  44. package/dist/types/utils/resolve-initial-context.d.ts +7 -0
  45. package/dist/types/version.d.ts +1 -0
  46. package/package.json +65 -0
  47. package/src/ContextConfigBuilder.ts +131 -0
  48. package/src/ContextProvider.ts +555 -0
  49. package/src/client/ContextClient.ts +71 -0
  50. package/src/configurator.ts +158 -0
  51. package/src/errors.ts +19 -0
  52. package/src/index.ts +18 -0
  53. package/src/module.ts +91 -0
  54. package/src/selectors.ts +41 -0
  55. package/src/types.ts +33 -0
  56. package/src/utils/enable-context.ts +31 -0
  57. package/src/utils/index.ts +2 -0
  58. package/src/utils/resolve-context-from-path.ts +34 -0
  59. package/src/utils/resolve-initial-context.ts +29 -0
  60. package/src/version.ts +2 -0
  61. package/tsconfig.json +33 -0
@@ -0,0 +1,555 @@
1
+ import { EMPTY, lastValueFrom, Observable, of, Subject, Subscription, throwError } from 'rxjs';
2
+ import {
3
+ catchError,
4
+ filter,
5
+ finalize,
6
+ map,
7
+ pairwise,
8
+ switchMap,
9
+ takeUntil,
10
+ tap,
11
+ } from 'rxjs/operators';
12
+
13
+ import { ContextModuleConfig } from './configurator';
14
+
15
+ import { ContextClient } from './client/ContextClient';
16
+ import { ContextItem, QueryContextParameters, RelatedContextParameters } from './types';
17
+ import { ModuleType } from '@equinor/fusion-framework-module';
18
+ import {
19
+ EventModule,
20
+ FrameworkEvent,
21
+ FrameworkEventInit,
22
+ } from '@equinor/fusion-framework-module-event';
23
+ import Query from '@equinor/fusion-query';
24
+
25
+ /**
26
+ * WARNING: this is an initial out cast.
27
+ * api clients will most probably not be exposed in future!
28
+ */
29
+ export interface IContextProvider {
30
+ /** DANGER */
31
+ readonly contextClient: ContextClient;
32
+ /** DANGER */
33
+ readonly queryClient: Query<ContextItem[], QueryContextParameters>;
34
+
35
+ readonly currentContext$: Observable<ContextItem | null | undefined>;
36
+ currentContext: ContextItem | null | undefined;
37
+ queryContext(search: string): Observable<Array<ContextItem>>;
38
+ queryContextAsync(search: string): Promise<Array<ContextItem>>;
39
+ validateContext(item: ContextItem<Record<string, unknown>>): boolean;
40
+ resolveContext: (current: ContextItem) => Observable<ContextItem>;
41
+ resolveContextAsync: (current: ContextItem) => Promise<ContextItem>;
42
+ relatedContexts: (
43
+ args: RelatedContextParameters,
44
+ ) => Observable<Array<ContextItem<Record<string, unknown>>>>;
45
+ relatedContextsAsync: (
46
+ args: RelatedContextParameters,
47
+ ) => Promise<Array<ContextItem<Record<string, unknown>>>>;
48
+ clearCurrentContext: VoidFunction;
49
+
50
+ setCurrentContextById(id: string): Observable<ContextItem<Record<string, unknown>>>;
51
+ setCurrentContextByIdAsync(id: string): Promise<ContextItem<Record<string, unknown>>>;
52
+
53
+ setCurrentContext(
54
+ context: ContextItem<Record<string, unknown>> | null,
55
+ opt?: { validate?: boolean; resolve?: boolean },
56
+ ): Observable<ContextItem<Record<string, unknown>> | null>;
57
+
58
+ setCurrentContextAsync(
59
+ context: ContextItem<Record<string, unknown>> | null,
60
+ opt?: { validate?: boolean; resolve?: boolean },
61
+ ): Promise<ContextItem<Record<string, unknown>> | null>;
62
+ }
63
+
64
+ export class ContextProvider implements IContextProvider {
65
+ #contextClient: ContextClient;
66
+ #contextQuery: Query<Array<ContextItem>, QueryContextParameters>;
67
+ #contextRelated?: Query<Array<ContextItem>, RelatedContextParameters>;
68
+
69
+ #event?: ModuleType<EventModule>;
70
+
71
+ #subscriptions = new Subscription();
72
+
73
+ #contextType?: ContextModuleConfig['contextType'];
74
+ #contextFilter: ContextModuleConfig['contextFilter'];
75
+ #contextParameterFn: Required<ContextModuleConfig['contextParameterFn']>;
76
+
77
+ #contextQueue = new Subject<Observable<ContextItem<Record<string, unknown>>>>();
78
+
79
+ public get contextClient() {
80
+ return this.#contextClient;
81
+ }
82
+
83
+ public get queryClient() {
84
+ return this.#contextQuery;
85
+ }
86
+
87
+ get currentContext$(): Observable<ContextItem | null | undefined> {
88
+ return this.#contextClient.currentContext$;
89
+ }
90
+
91
+ get currentContext(): ContextItem | undefined | null {
92
+ return this.#contextClient.currentContext;
93
+ }
94
+
95
+ /** @deprecated do not use, will be removed */
96
+ set currentContext(context: ContextItem | null | undefined) {
97
+ console.warn(
98
+ '@deprecated',
99
+ 'ContextProvider.currentContext',
100
+ 'use setCurrentContextById|setCurrentContext|clearCurrentContext',
101
+ );
102
+ if (context === undefined) {
103
+ throw Error('not allowed to set current context as undefined undefined!');
104
+ }
105
+ this.setCurrentContextAsync(context);
106
+ }
107
+
108
+ constructor(args: {
109
+ config: ContextModuleConfig;
110
+ event?: ModuleType<EventModule>;
111
+ /** @deprecated use ContextProvider.connectParentContext */
112
+ parentContext?: IContextProvider;
113
+ }) {
114
+ const { config, event } = args;
115
+
116
+ if (args.parentContext) {
117
+ console.warn(
118
+ '@deprecated',
119
+ 'parentContext as arg is deprecated, use ContextProvider.connectParentContext',
120
+ );
121
+ }
122
+
123
+ this.#event = event;
124
+
125
+ config.resolveContext && (this.resolveContext = config.resolveContext?.bind(this));
126
+ config.validateContext && (this.validateContext = config.validateContext?.bind(this));
127
+
128
+ this.#contextType = config.contextType;
129
+ this.#contextFilter = config.contextFilter;
130
+
131
+ this.#contextClient = new ContextClient(config.client.get);
132
+ this.#contextQuery = new Query(config.client.query);
133
+
134
+ if (config.client.related) {
135
+ this.#contextRelated = new Query(config.client.related);
136
+ }
137
+
138
+ this.#contextParameterFn =
139
+ config.contextParameterFn ??
140
+ ((args: Parameters<Required<ContextModuleConfig>['contextParameterFn']>[0]) => ({
141
+ search: args.search,
142
+ filter: { type: args.type },
143
+ }));
144
+
145
+ if (this.#event) {
146
+ this.#subscriptions.add(
147
+ this.currentContext$.pipe(pairwise()).subscribe(([previous, next]) => {
148
+ this.#event?.dispatchEvent('onCurrentContextChanged', {
149
+ source: this,
150
+ canBubble: true,
151
+ detail: { previous, next },
152
+ });
153
+ }),
154
+ );
155
+ this.#subscriptions.add(
156
+ /** observe event from child modules */
157
+ this.#event.addEventListener('onCurrentContextChanged', (e) => {
158
+ /** loop prevention */
159
+ if (e.source !== this && e.detail.next !== undefined) {
160
+ this.setCurrentContext(e.detail.next);
161
+ }
162
+ }),
163
+ );
164
+ }
165
+
166
+ this.#subscriptions.add(
167
+ this.#contextQueue
168
+ .pipe(
169
+ switchMap((next) => next),
170
+ tap((x) => console.debug('ContextProvider::#contextQueue', x)),
171
+ )
172
+ .subscribe((context) => this.#contextClient.setCurrentContext(context ?? null)),
173
+ );
174
+ }
175
+
176
+ public connectParentContext(
177
+ provider: IContextProvider,
178
+ opt?: { skipFirst: boolean },
179
+ ): Subscription {
180
+ const parentContext$ = provider.currentContext$.pipe(
181
+ // do not set context if parent has not initialized
182
+ filter((x): x is ContextItem | null => x !== undefined),
183
+ filter((next, index) => {
184
+ if (opt?.skipFirst && index <= 1) {
185
+ console.debug(
186
+ 'ContextProvider::connectParentContext',
187
+ 'skipping first item',
188
+ next,
189
+ );
190
+ return false;
191
+ }
192
+ return this.currentContext?.id !== next?.id;
193
+ }),
194
+ switchMap(async (next) => {
195
+ if (next) {
196
+ const onParentContextChanged = await this.#event?.dispatchEvent(
197
+ 'onParentContextChanged',
198
+ {
199
+ source: this,
200
+ detail: next,
201
+ cancelable: true,
202
+ },
203
+ );
204
+ return { next, canceled: onParentContextChanged?.canceled };
205
+ }
206
+ return { next };
207
+ }),
208
+ filter((x) => !x.canceled),
209
+ switchMap(({ next }) => {
210
+ return this.setCurrentContext(next, {
211
+ validate: true,
212
+ resolve: true,
213
+ }).pipe(
214
+ catchError((err) => {
215
+ console.warn(
216
+ 'ContextProvider::onParentContextChanged',
217
+ 'setCurrentContext',
218
+ err,
219
+ );
220
+ return EMPTY;
221
+ }),
222
+ );
223
+ }),
224
+ catchError((err) => {
225
+ console.warn('ContextProvider::onParentContextChanged', 'unhandled exception', err);
226
+ return EMPTY;
227
+ }),
228
+ );
229
+
230
+ const subscription = parentContext$.subscribe();
231
+ this.#subscriptions.add(subscription);
232
+ return subscription;
233
+ }
234
+
235
+ public setCurrentContextById(id: string): Observable<ContextItem<Record<string, unknown>>> {
236
+ return new Observable((subscriber) => {
237
+ try {
238
+ this.#contextClient
239
+ .resolveContext(id)
240
+ .pipe(
241
+ filter((item): item is ContextItem => !!item),
242
+ switchMap((item) => this.setCurrentContext(item)),
243
+ )
244
+ .subscribe(subscriber);
245
+ } catch (err) {
246
+ subscriber.error(err);
247
+ }
248
+ });
249
+ }
250
+
251
+ public setCurrentContextByIdAsync(id: string): Promise<ContextItem<Record<string, unknown>>> {
252
+ return lastValueFrom(this.setCurrentContextById(id));
253
+ }
254
+
255
+ /**
256
+ * Setting context is a complex operation, and might not happen immediately.
257
+ * When setting the context, a task is created and added to the queue.
258
+ * Once the task is completed, the returned observable will emit the value which will be the next state.
259
+ *
260
+ * Even tho this function returns a `Observable`, the task will be queued even tho nobody subscribes.
261
+ *
262
+ * If the observable is subscribe, unsubscribing __WILL__ abort the task and remove it from queue
263
+ *
264
+ * @param context context item which would be queue to set as current
265
+ */
266
+ public setCurrentContext<T extends ContextItem<Record<string, unknown>> | null>(
267
+ context: T,
268
+ opt?: { validate?: boolean; resolve?: boolean },
269
+ ): Observable<T> {
270
+ /** signal for aborting the queue entry */
271
+ const abort$ = new Subject();
272
+
273
+ /** wrapper for returning an observable to the caller */
274
+ const subject$ = new Subject<T>();
275
+
276
+ const task$ = this._setCurrentContext(context, opt).pipe(
277
+ /** send context item which was set to the caller */
278
+ tap((x) => subject$.next(x)),
279
+ /** abort task on signal */
280
+ takeUntil(abort$),
281
+ /** close observable sent to caller */
282
+ finalize(() => subject$.complete()),
283
+ /** catch errors to not stall queue */
284
+ catchError((err) => {
285
+ /** send the error to the caller */
286
+ subject$.error(err);
287
+ /** skip setting any context */
288
+ return EMPTY;
289
+ }),
290
+ );
291
+
292
+ /** add task to internal queue */
293
+ this.#contextQueue.next(task$ as Observable<ContextItem<Record<string, unknown>>>);
294
+
295
+ return subject$.pipe(
296
+ /** if caller subscribes, unsubscribe should abort queue entry */
297
+ finalize(() => abort$.next(true)),
298
+ );
299
+ }
300
+
301
+ protected _setCurrentContext<T extends ContextItem<Record<string, unknown>> | null>(
302
+ context: T,
303
+ opt?: { validate?: boolean; resolve?: boolean },
304
+ ): Observable<T> {
305
+ return new Observable((subscriber) => {
306
+ if (context === this.currentContext) {
307
+ subscriber.next(context);
308
+ return subscriber.complete();
309
+ }
310
+ if (context && opt?.validate && !this.validateContext(context)) {
311
+ if (!opt.resolve) {
312
+ /** cannot resolve context, and provided is invalid */
313
+ this.#event?.dispatchEvent('onSetContextValidationFailed', {
314
+ source: this,
315
+ detail: { context },
316
+ });
317
+ return subscriber.error(Error('failed to validate provided context'));
318
+ }
319
+ if (opt.resolve) {
320
+ return of(context)
321
+ .pipe(
322
+ /** notify event observers that context is about to get resolved */
323
+ switchMap(async (context) => {
324
+ const event = await this.#event?.dispatchEvent(
325
+ 'onSetContextResolve',
326
+ {
327
+ source: this,
328
+ cancelable: true,
329
+ detail: { context },
330
+ },
331
+ );
332
+ if (event?.canceled) {
333
+ throw Error('resolving of context was canceled');
334
+ }
335
+ return context;
336
+ }),
337
+ /** execute resolve */
338
+ switchMap((context) =>
339
+ this.resolveContext(context).pipe(
340
+ map((resolved) => ({
341
+ context,
342
+ resolved,
343
+ })),
344
+ ),
345
+ ),
346
+ /** notify event observers that context was resolved */
347
+ switchMap(async ({ context, resolved }) => {
348
+ const event = await this.#event?.dispatchEvent(
349
+ 'onSetContextResolved',
350
+ {
351
+ source: this,
352
+ cancelable: true,
353
+ detail: { context, resolved },
354
+ },
355
+ );
356
+ if (event?.canceled) {
357
+ throw Error('resolving of context was canceled');
358
+ }
359
+ return resolved;
360
+ }),
361
+ /** recursive set current context without validation and resolve */
362
+ switchMap((resolved) =>
363
+ this._setCurrentContext(resolved as unknown as T),
364
+ ),
365
+ )
366
+ .subscribe(subscriber);
367
+ }
368
+ }
369
+
370
+ return of(context)
371
+ .pipe(
372
+ switchMap(async (context) => {
373
+ const event = await this.#event?.dispatchEvent('onCurrentContextChange', {
374
+ source: this,
375
+ canBubble: true,
376
+ cancelable: true,
377
+ detail: { context: context },
378
+ });
379
+
380
+ if (event?.canceled) {
381
+ throw Error('change of context was aborted');
382
+ }
383
+
384
+ return context;
385
+ }),
386
+ )
387
+ .subscribe((context) => {
388
+ subscriber.next(context);
389
+ subscriber.complete();
390
+ });
391
+ });
392
+ }
393
+
394
+ public async setCurrentContextAsync<T extends ContextItem<Record<string, unknown>> | null>(
395
+ context: T,
396
+ opt?: { validate?: boolean; resolve?: boolean },
397
+ ): Promise<T> {
398
+ return lastValueFrom(this.setCurrentContext(context, opt));
399
+ }
400
+
401
+ public queryContext(search: string): Observable<Array<ContextItem>> {
402
+ const query$ = this.queryClient
403
+ .query(
404
+ // TODO
405
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
406
+ /* @ts-ignore */
407
+ this.#contextParameterFn({ search, type: this.#contextType }),
408
+ )
409
+ .pipe(map((x) => x.value));
410
+
411
+ return this.#contextFilter ? query$.pipe(map(this.#contextFilter)) : query$;
412
+ }
413
+
414
+ public queryContextAsync(search: string): Promise<Array<ContextItem>> {
415
+ return lastValueFrom(this.queryContext(search));
416
+ }
417
+
418
+ public validateContext(item: ContextItem<Record<string, unknown>>): boolean {
419
+ if (!this.#contextType) return true;
420
+ return this.#contextType.map((x) => x.toLowerCase()).includes(item.type.id.toLowerCase());
421
+ }
422
+
423
+ public resolveContext(
424
+ item: ContextItem<Record<string, unknown>>,
425
+ ): Observable<ContextItem<Record<string, unknown>>> {
426
+ return this.relatedContexts({ item, filter: { type: this.#contextType } }).pipe(
427
+ map((x) => x.filter((item) => this.validateContext(item))),
428
+ map((values) => {
429
+ const value = values.shift();
430
+ if (!value) {
431
+ throw Error('failed to resolve context');
432
+ }
433
+ if (values.length) {
434
+ console.warn(
435
+ 'ContextProvider::relatedContext',
436
+ 'multiple items found 🤣',
437
+ values,
438
+ );
439
+ }
440
+ return value;
441
+ }),
442
+ );
443
+ }
444
+
445
+ public resolveContextAsync(
446
+ item: ContextItem<Record<string, unknown>>,
447
+ ): Promise<ContextItem<Record<string, unknown>>> {
448
+ return lastValueFrom(this.resolveContext(item));
449
+ }
450
+
451
+ public relatedContexts(
452
+ args: RelatedContextParameters,
453
+ ): Observable<Array<ContextItem<Record<string, unknown>>>> {
454
+ if (!this.#contextRelated) {
455
+ return throwError(() =>
456
+ Error(
457
+ 'ContextProvider::relatedContexts - no client defined for resolving related context',
458
+ ),
459
+ );
460
+ }
461
+ return this.#contextRelated.query(args).pipe(
462
+ map(({ value }) => value),
463
+ catchError((err) => {
464
+ if (err.cause) {
465
+ throw err.cause;
466
+ }
467
+ throw err;
468
+ }),
469
+ );
470
+ }
471
+
472
+ public relatedContextsAsync(
473
+ args: RelatedContextParameters,
474
+ ): Promise<Array<ContextItem<Record<string, unknown>>>> {
475
+ return lastValueFrom(this.relatedContexts(args));
476
+ }
477
+
478
+ public clearCurrentContext(): void {
479
+ this.setCurrentContext(null);
480
+ }
481
+
482
+ dispose() {
483
+ this.#subscriptions.unsubscribe();
484
+ this.#contextClient.dispose();
485
+ }
486
+ }
487
+
488
+ export default ContextProvider;
489
+
490
+ declare module '@equinor/fusion-framework-module-event' {
491
+ interface FrameworkEventMap {
492
+ /** dispatch before context changes */
493
+ onCurrentContextChange: FrameworkEvent<
494
+ FrameworkEventInit<
495
+ {
496
+ context: ContextItem | null;
497
+ },
498
+ IContextProvider
499
+ >
500
+ >;
501
+ /** dispatch after context changed */
502
+ onCurrentContextChanged: FrameworkEvent<
503
+ FrameworkEventInit<
504
+ {
505
+ next: ContextItem | null;
506
+ previous?: ContextItem | null;
507
+ },
508
+ IContextProvider
509
+ >
510
+ >;
511
+ onParentContextChanged: FrameworkEvent<
512
+ FrameworkEventInit<
513
+ {
514
+ context: ContextItem | null;
515
+ },
516
+ IContextProvider
517
+ >
518
+ >;
519
+
520
+ onSetContextResolve: FrameworkEvent<
521
+ FrameworkEventInit<
522
+ {
523
+ context: ContextItem;
524
+ },
525
+ IContextProvider
526
+ >
527
+ >;
528
+ onSetContextResolved: FrameworkEvent<
529
+ FrameworkEventInit<
530
+ {
531
+ context: ContextItem;
532
+ resolved?: ContextItem | null;
533
+ },
534
+ IContextProvider
535
+ >
536
+ >;
537
+ onSetContextValidationFailed: FrameworkEvent<
538
+ FrameworkEventInit<
539
+ {
540
+ context: ContextItem;
541
+ },
542
+ IContextProvider
543
+ >
544
+ >;
545
+ onSetContextResolveFailed: FrameworkEvent<
546
+ FrameworkEventInit<
547
+ {
548
+ context: ContextItem;
549
+ error: unknown;
550
+ },
551
+ IContextProvider
552
+ >
553
+ >;
554
+ }
555
+ }
@@ -0,0 +1,71 @@
1
+ import { Observable, BehaviorSubject, EMPTY, lastValueFrom, firstValueFrom } from 'rxjs';
2
+ import { catchError, map } from 'rxjs/operators';
3
+
4
+ import equal from 'fast-deep-equal';
5
+
6
+ import { Query, QueryCtorOptions } from '@equinor/fusion-query';
7
+
8
+ import { ContextItem } from '../types';
9
+
10
+ export type GetContextParameters = { id: string };
11
+
12
+ export class ContextClient extends Observable<ContextItem | null> {
13
+ #client: Query<ContextItem, { id: string }>;
14
+ /** might change to reactive state, for comparing state with reducer */
15
+ #currentContext$: BehaviorSubject<ContextItem | null | undefined>;
16
+
17
+ get currentContext(): ContextItem | null | undefined {
18
+ return this.#currentContext$.value;
19
+ }
20
+
21
+ get currentContext$(): Observable<ContextItem | null | undefined> {
22
+ return this.#currentContext$.asObservable();
23
+ }
24
+
25
+ get client(): Query<ContextItem, { id: string }> {
26
+ return this.#client;
27
+ }
28
+
29
+ constructor(options: QueryCtorOptions<ContextItem, GetContextParameters>) {
30
+ super((observer) => this.#currentContext$.subscribe(observer));
31
+ this.#client = new Query(options);
32
+ this.#currentContext$ = new BehaviorSubject<ContextItem | null | undefined>(undefined);
33
+ }
34
+
35
+ public setCurrentContext(idOrItem?: string | ContextItem | null) {
36
+ if (typeof idOrItem === 'string') {
37
+ // TODO - compare context
38
+ this.resolveContext(idOrItem)
39
+ // TODO should this catch error?
40
+ .pipe(catchError(() => EMPTY))
41
+ .subscribe((value) => this.setCurrentContext(value));
42
+ /** only add context if not match */
43
+ } else if (!equal(idOrItem, this.#currentContext$.value)) {
44
+ this.#currentContext$.next(idOrItem);
45
+ }
46
+ }
47
+
48
+ public resolveContext(id: string): Observable<ContextItem> {
49
+ return this.#client.query({ id }).pipe(
50
+ map((x) => x.value),
51
+ // unwrap error
52
+ catchError((err) => {
53
+ if (err.cause) {
54
+ throw err.cause;
55
+ }
56
+ throw err;
57
+ }),
58
+ );
59
+ }
60
+
61
+ public resolveContextAsync(id: string, opt?: { awaitResolve: boolean }): Promise<ContextItem> {
62
+ const fn = opt?.awaitResolve ? lastValueFrom : firstValueFrom;
63
+ return fn(this.resolveContext(id));
64
+ }
65
+
66
+ public dispose(): void {
67
+ this.#currentContext$.complete();
68
+ }
69
+ }
70
+
71
+ export default ContextClient;