@equinor/fusion-framework-module-context 7.0.4-next.0 → 8.0.1

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 (79) hide show
  1. package/CHANGELOG.md +36 -15
  2. package/README.md +168 -16
  3. package/dist/esm/ContextConfigBuilder.js +24 -6
  4. package/dist/esm/ContextConfigBuilder.js.map +1 -1
  5. package/dist/esm/ContextProvider.js +71 -15
  6. package/dist/esm/ContextProvider.js.map +1 -1
  7. package/dist/esm/client/ContextClient.js +13 -3
  8. package/dist/esm/client/ContextClient.js.map +1 -1
  9. package/dist/esm/configurator.js +40 -5
  10. package/dist/esm/configurator.js.map +1 -1
  11. package/dist/esm/{errors.js → errors/FusionContextSearchError.js} +3 -1
  12. package/dist/esm/errors/FusionContextSearchError.js.map +1 -0
  13. package/dist/esm/errors/index.js +2 -0
  14. package/dist/esm/errors/index.js.map +1 -0
  15. package/dist/esm/get-context-selector.js +14 -0
  16. package/dist/esm/get-context-selector.js.map +1 -0
  17. package/dist/esm/index.js +12 -0
  18. package/dist/esm/index.js.map +1 -1
  19. package/dist/esm/module.js +6 -0
  20. package/dist/esm/module.js.map +1 -1
  21. package/dist/esm/parse-context-item.js +31 -0
  22. package/dist/esm/parse-context-item.js.map +1 -0
  23. package/dist/esm/query-context-selector.js +12 -0
  24. package/dist/esm/query-context-selector.js.map +1 -0
  25. package/dist/esm/related-context-selector.js +12 -0
  26. package/dist/esm/related-context-selector.js.map +1 -0
  27. package/dist/esm/utils/extract-context-id-from-path.js +25 -0
  28. package/dist/esm/utils/extract-context-id-from-path.js.map +1 -0
  29. package/dist/esm/utils/index.js +10 -0
  30. package/dist/esm/utils/index.js.map +1 -1
  31. package/dist/esm/utils/resolve-context-from-path.js +11 -22
  32. package/dist/esm/utils/resolve-context-from-path.js.map +1 -1
  33. package/dist/esm/utils/resolve-initial-context.js +2 -0
  34. package/dist/esm/utils/resolve-initial-context.js.map +1 -1
  35. package/dist/esm/version.js +1 -1
  36. package/dist/esm/version.js.map +1 -1
  37. package/dist/tsconfig.tsbuildinfo +1 -1
  38. package/dist/types/ContextConfigBuilder.d.ts +31 -1
  39. package/dist/types/ContextProvider.d.ts +74 -3
  40. package/dist/types/client/ContextClient.d.ts +8 -1
  41. package/dist/types/configurator.d.ts +134 -4
  42. package/dist/types/{errors.d.ts → errors/FusionContextSearchError.d.ts} +2 -0
  43. package/dist/types/errors/index.d.ts +1 -0
  44. package/dist/types/get-context-selector.d.ts +9 -0
  45. package/dist/types/index.d.ts +12 -0
  46. package/dist/types/module.d.ts +11 -0
  47. package/dist/types/parse-context-item.d.ts +8 -0
  48. package/dist/types/query-context-selector.d.ts +7 -0
  49. package/dist/types/related-context-selector.d.ts +7 -0
  50. package/dist/types/types.d.ts +23 -1
  51. package/dist/types/utils/extract-context-id-from-path.d.ts +16 -0
  52. package/dist/types/utils/index.d.ts +10 -0
  53. package/dist/types/utils/resolve-context-from-path.d.ts +1 -16
  54. package/dist/types/utils/resolve-initial-context.d.ts +1 -1
  55. package/dist/types/version.d.ts +1 -1
  56. package/package.json +12 -12
  57. package/src/ContextConfigBuilder.ts +47 -7
  58. package/src/ContextProvider.ts +115 -28
  59. package/src/client/ContextClient.ts +13 -3
  60. package/src/configurator.ts +152 -10
  61. package/src/{errors.ts → errors/FusionContextSearchError.ts} +2 -0
  62. package/src/errors/index.ts +1 -0
  63. package/src/get-context-selector.ts +18 -0
  64. package/src/index.ts +13 -0
  65. package/src/module.ts +11 -0
  66. package/src/parse-context-item.ts +39 -0
  67. package/src/query-context-selector.ts +15 -0
  68. package/src/related-context-selector.ts +15 -0
  69. package/src/types.ts +23 -1
  70. package/src/utils/extract-context-id-from-path.ts +30 -0
  71. package/src/utils/index.ts +10 -0
  72. package/src/utils/resolve-context-from-path.ts +13 -28
  73. package/src/utils/resolve-initial-context.ts +2 -0
  74. package/src/version.ts +1 -1
  75. package/dist/esm/errors.js.map +0 -1
  76. package/dist/esm/selectors.js +0 -58
  77. package/dist/esm/selectors.js.map +0 -1
  78. package/dist/types/selectors.d.ts +0 -19
  79. package/src/selectors.ts +0 -70
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equinor/fusion-framework-module-context",
3
- "version": "7.0.4-next.0",
3
+ "version": "8.0.1",
4
4
  "description": "",
5
5
  "main": "./dist/esm/index.js",
6
6
  "exports": {
@@ -9,8 +9,8 @@
9
9
  "types": "./dist/types/index.d.ts"
10
10
  },
11
11
  "./errors.js": {
12
- "import": "./dist/esm/errors.js",
13
- "types": "./dist/types/errors.d.ts"
12
+ "import": "./dist/esm/errors/index.js",
13
+ "types": "./dist/types/errors/index.d.ts"
14
14
  },
15
15
  "./utils": {
16
16
  "import": "./dist/esm/utils/index.js",
@@ -25,7 +25,7 @@
25
25
  "dist/types/index.d.ts"
26
26
  ],
27
27
  "errors.js": [
28
- "dist/types/errors.d.ts"
28
+ "dist/types/errors/index.d.ts"
29
29
  ],
30
30
  "utils": [
31
31
  "dist/types/utils/index.d.ts"
@@ -45,19 +45,19 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "fast-deep-equal": "^3.1.3",
48
- "@equinor/fusion-query": "^6.0.5-next.0"
48
+ "@equinor/fusion-query": "^7.0.2"
49
49
  },
50
50
  "devDependencies": {
51
51
  "rxjs": "^7.8.1",
52
- "typescript": "^5.8.2",
53
- "@equinor/fusion-framework-module": "^5.0.7-next.0",
54
- "@equinor/fusion-framework-module-event": "^5.0.2-next.0",
55
- "@equinor/fusion-framework-module-navigation": "^7.0.0-next.2",
56
- "@equinor/fusion-framework-module-services": "^7.2.2-next.0"
52
+ "typescript": "^7.0.2",
53
+ "@equinor/fusion-framework-module-navigation": "^7.0.5",
54
+ "@equinor/fusion-framework-module": "^6.1.1",
55
+ "@equinor/fusion-framework-module-services": "^8.0.2",
56
+ "@equinor/fusion-framework-module-event": "^6.0.1"
57
57
  },
58
58
  "peerDependencies": {
59
- "rxjs": "^7.8.1",
60
- "@equinor/fusion-framework-module": "^5.0.7-next.0"
59
+ "rxjs": "^7.0.0",
60
+ "@equinor/fusion-framework-module": "^6.1.1"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsc -b"
@@ -17,6 +17,14 @@ import type {
17
17
 
18
18
  import type { ContextItem, QueryContextParameters, RelatedContextParameters } from './types';
19
19
 
20
+ /**
21
+ * Callback passed to {@link IContextModuleConfigurator.addConfigBuilder}.
22
+ *
23
+ * Receives a {@link ContextConfigBuilder} and may use its setter methods
24
+ * to populate the context module configuration. The callback may be async.
25
+ *
26
+ * @typeParam TDeps - Module dependency array inferred from the configurator.
27
+ */
20
28
  export type ContextConfigBuilderCallback = <TDeps extends Array<AnyModule> = []>(
21
29
  builder: ContextConfigBuilder<TDeps, ModuleInitializerArgs<IContextModuleConfigurator, TDeps>>,
22
30
  ) => void | Promise<void>;
@@ -44,19 +52,25 @@ export type ContextConfigBuilderCallback = <TDeps extends Array<AnyModule> = []>
44
52
  * - The builder pattern allows chaining configuration methods for clarity and convenience.
45
53
  * - The `requireInstance` method enables asynchronous retrieval of module instances by name.
46
54
  *
47
- * @todo - this should extend the BaseConfigBuilder
55
+ * @todo(#5120) - this should extend the BaseConfigBuilder
48
56
  *
49
57
  * @see ContextModuleConfig
50
58
  * @see ModuleInitializerArgs
51
59
  */
52
60
  export class ContextConfigBuilder<
53
61
  TModules extends Array<AnyModule> = [],
62
+ // biome-ignore lint/suspicious/noExplicitAny: `ModuleInitializerArgs<any, any>` widens the constraint to accept initializer args for any configurator/module set \u2014 `unknown` breaks assignability of the concrete default type argument
54
63
  TInit extends ModuleInitializerArgs<any, any> = ModuleInitializerArgs<
55
64
  ContextModuleConfigurator,
56
65
  TModules
57
66
  >,
58
67
  > {
59
68
  #init: TInit;
69
+ /**
70
+ * Creates a new `ContextConfigBuilder`.
71
+ * @param init - Module initializer arguments used to resolve module instances.
72
+ * @param config - The partial context module config to build upon.
73
+ */
60
74
  constructor(
61
75
  init: TInit,
62
76
  public config: Partial<ContextModuleConfig> = {},
@@ -64,13 +78,31 @@ export class ContextConfigBuilder<
64
78
  this.#init = init;
65
79
  }
66
80
 
81
+ /**
82
+ * Requires a module instance by its registered key.
83
+ * @param module - The key of the module to resolve.
84
+ * @returns A promise that resolves to the requested module instance.
85
+ */
67
86
  requireInstance<TKey extends string = Extract<keyof Modules, string>>(
68
87
  module: TKey,
69
88
  ): Promise<ModuleType<Modules[TKey]>>;
70
89
 
90
+ /**
91
+ * Requires a module instance by name.
92
+ * @param module - The name of the module to resolve.
93
+ * @returns A promise that resolves to the requested module instance.
94
+ */
71
95
  requireInstance<T>(module: string): Promise<T>;
72
96
 
73
- requireInstance(module: string): Promise<any> {
97
+ /**
98
+ * Requires a module instance by key or name.
99
+ * @param module - The key or name of the module to resolve.
100
+ * @returns A promise that resolves to the requested module instance.
101
+ */
102
+ requireInstance(
103
+ module: string,
104
+ // biome-ignore lint/suspicious/noExplicitAny: implementation signature must satisfy both overloads above (`Promise<ModuleType<...>>` and `Promise<T>`); `unknown` is not assignable to the generic `Promise<T>` overload
105
+ ): Promise<any> {
74
106
  return this.#init.requireInstance(module);
75
107
  }
76
108
 
@@ -147,6 +179,13 @@ export class ContextConfigBuilder<
147
179
  this.config.generatePathFromContext = fn;
148
180
  }
149
181
 
182
+ /**
183
+ * Sets the function used to resolve the initial context during module post-initialization.
184
+ *
185
+ * @param fn - A function that returns an observable input emitting the initial context item.
186
+ * The default resolver extracts a context ID from the navigation path, falling
187
+ * back to the parent provider's current context.
188
+ */
150
189
  setResolveInitialContext(fn: ContextModuleConfig['resolveInitialContext']) {
151
190
  this.config.resolveInitialContext = fn;
152
191
  }
@@ -177,7 +216,7 @@ export class ContextConfigBuilder<
177
216
  },
178
217
  expire = 1 * 60 * 1000,
179
218
  ): void {
180
- this.config.client = {
219
+ const clientConfig: ContextModuleConfig['client'] = {
181
220
  get:
182
221
  typeof client.get === 'function'
183
222
  ? {
@@ -191,7 +230,7 @@ export class ContextConfigBuilder<
191
230
  query:
192
231
  typeof client.query === 'function'
193
232
  ? {
194
- // TODO - might cast to checksum
233
+ // TODO(#5118) - might cast to checksum
195
234
  key: (args) => JSON.stringify(args),
196
235
  client: {
197
236
  fn: client.query,
@@ -200,12 +239,13 @@ export class ContextConfigBuilder<
200
239
  }
201
240
  : client.query,
202
241
  };
242
+ this.config.client = clientConfig;
243
+ // only override the related-context client config if one was provided
203
244
  if (client.related) {
204
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
205
- this.config.client!.related =
245
+ clientConfig.related =
206
246
  typeof client.related === 'function'
207
247
  ? {
208
- // TODO - might cast to checksum
248
+ // TODO(#5118) - might cast to checksum
209
249
  key: (args) => JSON.stringify(args),
210
250
  client: {
211
251
  fn: client.related,
@@ -346,35 +346,64 @@ export class ContextProvider
346
346
 
347
347
  #contextQueue = new Subject<Observable<ContextItem<Record<string, unknown>>>>();
348
348
 
349
+ /**
350
+ * The underlying context client used to resolve and hold the current context item.
351
+ * @returns The internal {@link ContextClient} instance.
352
+ */
349
353
  public get contextClient() {
350
354
  return this.#contextClient;
351
355
  }
352
356
 
357
+ /**
358
+ * The query client used to search for context items.
359
+ * @returns The internal `Query` instance used by {@link queryContext}.
360
+ */
353
361
  public get queryClient() {
354
362
  return this.#contextQuery;
355
363
  }
356
364
 
365
+ /**
366
+ * Observable stream emitting the current context item.
367
+ * @returns Observable that emits the current `ContextItem`, `null`, or `undefined`.
368
+ */
357
369
  get currentContext$(): Observable<ContextItem | null | undefined> {
358
370
  return this.#contextClient.currentContext$;
359
371
  }
360
372
 
373
+ /**
374
+ * Snapshot of the current context item.
375
+ * @returns The current `ContextItem`, or `null`/`undefined` if not set.
376
+ */
361
377
  get currentContext(): ContextItem | undefined | null {
362
378
  return this.#contextClient.currentContext;
363
379
  }
364
380
 
365
- /** @deprecated do not use, will be removed */
381
+ /**
382
+ * Sets the current context item.
383
+ * @deprecated do not use, will be removed
384
+ * @param context - The context item to set as current. Must not be `undefined`.
385
+ * @throws Error if `context` is `undefined`.
386
+ */
366
387
  set currentContext(context: ContextItem | null | undefined) {
367
388
  console.warn(
368
389
  '@deprecated',
369
390
  'ContextProvider.currentContext',
370
391
  'use setCurrentContextById|setCurrentContext|clearCurrentContext',
371
392
  );
393
+ // undefined is reserved to mean "not yet initialized", so it cannot be set explicitly
372
394
  if (context === undefined) {
373
395
  throw Error('not allowed to set current context as undefined undefined!');
374
396
  }
375
397
  this.setCurrentContextAsync(context);
376
398
  }
377
399
 
400
+ /**
401
+ * Creates a new instance of `ContextProvider`.
402
+ * @param args - Constructor arguments.
403
+ * @param args.config - The context module configuration.
404
+ * @param args.event - Optional event module instance for dispatching context change events.
405
+ * @param args.parentContext - Optional parent context provider. Deprecated, use {@link connectParentContext}.
406
+ */
378
407
  constructor(args: {
379
408
  config: ContextModuleConfig;
380
409
  event?: ModuleType<EventModule>;
@@ -385,6 +414,7 @@ export class ContextProvider
385
414
 
386
415
  super({ version, config });
387
416
 
417
+ // warn about deprecated parentContext constructor arg
388
418
  if (args.parentContext) {
389
419
  console.warn(
390
420
  '@deprecated',
@@ -398,16 +428,19 @@ export class ContextProvider
398
428
  if (config.resolveContext) {
399
429
  this.resolveContext = config.resolveContext?.bind(this);
400
430
  }
431
+ // override validateContext if configured
401
432
  if (config.validateContext) {
402
433
  this.validateContext = config.validateContext?.bind(this);
403
434
  }
404
435
 
436
+ // override extractContextIdFromPath if configured
405
437
  if (config.extractContextIdFromPath) {
406
- // @ts-ignore
438
+ // @ts-expect-error
407
439
  this.extractContextIdFromPath = config.extractContextIdFromPath;
408
440
  }
441
+ // override generatePathFromContext if configured
409
442
  if (config.generatePathFromContext) {
410
- // @ts-ignore
443
+ // @ts-expect-error
411
444
  this.generatePathFromContext = config.generatePathFromContext;
412
445
  }
413
446
 
@@ -418,6 +451,7 @@ export class ContextProvider
418
451
  this.#contextClient = new ContextClient(config.client.get);
419
452
  this.#contextQuery = new Query(config.client.query);
420
453
 
454
+ // only create the related-context query when the config provides one
421
455
  if (config.client.related) {
422
456
  this.#contextRelated = new Query(config.client.related);
423
457
  }
@@ -436,10 +470,8 @@ export class ContextProvider
436
470
  this.#subscriptions.add(
437
471
  // observe current context changes
438
472
  this.currentContext$
439
- .pipe(
440
- // emit previous and next context
441
- pairwise(),
442
- )
473
+ // emit previous and next context together for change comparisons
474
+ .pipe(pairwise())
443
475
  .subscribe(([previous, next]) => {
444
476
  this.#event?.dispatchEvent('onCurrentContextChanged', {
445
477
  source: this,
@@ -494,12 +526,13 @@ export class ContextProvider
494
526
  provider: IContextProvider,
495
527
  opt?: { skipFirst: boolean },
496
528
  ): Subscription {
529
+ // build a stream of validated context changes from the parent provider
497
530
  const parentContext$ = provider.currentContext$.pipe(
498
531
  // do not set context if parent has not initialized
499
532
  filter((x): x is ContextItem | null => x !== undefined),
500
533
  filter((next, index) => {
501
534
  // skip first item if opt.skipFirst is true
502
- // TODO: this is a bit hacky, should be handled in a better way
535
+ // TODO(#5121): this is a bit hacky, should be handled in a better way
503
536
  if (opt?.skipFirst && index <= 1) {
504
537
  console.debug('ContextProvider::connectParentContext', 'skipping first item', next);
505
538
  return false;
@@ -508,8 +541,8 @@ export class ContextProvider
508
541
  return this.currentContext?.id !== next?.id;
509
542
  }),
510
543
  switchMap(async (next) => {
544
+ // if parent context is null, just return
511
545
  if (!next) {
512
- // if parent context is null, just return
513
546
  return { next };
514
547
  }
515
548
  // notify event observers that parent context is about to change and await for cancelation
@@ -524,15 +557,19 @@ export class ContextProvider
524
557
  filter((x) => !x.canceled),
525
558
  switchMap(({ next }) => {
526
559
  // set current context with validation and resolution
527
- return this.setCurrentContext(next, {
528
- validate: true,
529
- resolve: true,
530
- }).pipe(
531
- catchError((err) => {
532
- console.warn('ContextProvider::onParentContextChanged', 'setCurrentContext', err);
533
- // do not emit any value if an error occurs
534
- return EMPTY;
535
- }),
560
+ return (
561
+ this.setCurrentContext(next, {
562
+ validate: true,
563
+ resolve: true,
564
+ })
565
+ // swallow errors so a failed context change doesn't break the parent subscription
566
+ .pipe(
567
+ catchError((err) => {
568
+ console.warn('ContextProvider::onParentContextChanged', 'setCurrentContext', err);
569
+ // do not emit any value if an error occurs
570
+ return EMPTY;
571
+ }),
572
+ )
536
573
  );
537
574
  }),
538
575
  catchError((err) => {
@@ -568,6 +605,7 @@ export class ContextProvider
568
605
  this.#contextClient
569
606
  // resolve context item by id
570
607
  .resolveContext(id)
608
+ // filter out invalid items and set as current context
571
609
  .pipe(
572
610
  // filter out invalid context items
573
611
  filter((item): item is ContextItem => !!item),
@@ -609,6 +647,11 @@ export class ContextProvider
609
647
  * If the observable is subscribe, unsubscribing __WILL__ abort the task and remove it from queue
610
648
  *
611
649
  * @param context context item which would be queue to set as current
650
+ * @param opt Optional settings.
651
+ * @param opt.validate Whether to validate the context item before setting it.
652
+ * @param opt.resolve Whether to attempt to resolve the context item if validation fails.
653
+ * @template T The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
654
+ * @returns An observable that emits the context item once the queued task completes.
612
655
  */
613
656
  public setCurrentContext<T extends ContextItem<Record<string, unknown>> | null>(
614
657
  context: T,
@@ -620,6 +663,7 @@ export class ContextProvider
620
663
  // wrapper for returning an observable to the caller
621
664
  const subject$ = new Subject<T>();
622
665
 
666
+ // run the actual context-setting logic and relay results/errors to the caller's subject
623
667
  const task$ = this._setCurrentContext(context, opt).pipe(
624
668
  // send context item which was set to the caller
625
669
  tap((x) => subject$.next(x)),
@@ -639,6 +683,7 @@ export class ContextProvider
639
683
  // add task to internal queue
640
684
  this.#contextQueue.next(task$ as Observable<ContextItem<Record<string, unknown>>>);
641
685
 
686
+ // tear down the queued task if the caller unsubscribes
642
687
  return subject$.pipe(
643
688
  // if caller subscribes, unsubscribe should abort queue entry
644
689
  finalize(() => abort$.next(true)),
@@ -674,7 +719,7 @@ export class ContextProvider
674
719
  * - Emit the context and complete the observable.
675
720
  *
676
721
  * @protected
677
- * @typeParam T - The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
722
+ * @template T - The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
678
723
  * @param context - The new context to set.
679
724
  * @param opt - Optional settings:
680
725
  * - `validate`: Whether to validate the context before setting.
@@ -708,7 +753,10 @@ export class ContextProvider
708
753
  // emit error and complete
709
754
  return subscriber.error(Error('failed to validate provided context'));
710
755
  }
756
+ // if resolve is enabled, attempt to resolve the invalid context before setting it
711
757
  if (opt.resolve) {
758
+ // the recursive `_setCurrentContext` call below is re-entered with the already
759
+ // validated/resolved context, so the generic `T` cast is safe here.
712
760
  return of(context)
713
761
  .pipe(
714
762
  // notify event observers that context is about to get resolved
@@ -727,6 +775,7 @@ export class ContextProvider
727
775
  }),
728
776
  // resolve context
729
777
  switchMap((context) =>
778
+ // Pair the resolved context alongside the original for downstream consumers.
730
779
  this.resolveContext(context).pipe(
731
780
  map((resolved) => ({
732
781
  context,
@@ -789,7 +838,7 @@ export class ContextProvider
789
838
  *
790
839
  * @see {@link setCurrentContext} for more details.
791
840
  *
792
- * @typeParam T - The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
841
+ * @template T - The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
793
842
  * @param context - The context item to set as the current context, or `null` to clear it.
794
843
  * @param opt - Optional settings for context handling.
795
844
  * @param opt.validate - If `true`, validates the context before setting it.
@@ -813,6 +862,7 @@ export class ContextProvider
813
862
  *
814
863
  * @param search - The search string to filter context items.
815
864
  * @returns An Observable that emits an array of `ContextItem` objects matching the search criteria.
865
+ * @throws Re-throws the underlying cause of a `QueryClientError`, otherwise re-throws the original error.
816
866
  */
817
867
  public queryContext(search: string): Observable<Array<ContextItem>> {
818
868
  const query$ = this.queryClient
@@ -823,6 +873,7 @@ export class ContextProvider
823
873
  type: this.#contextType,
824
874
  }) as QueryContextParameters,
825
875
  )
876
+ // unwrap query-client errors and expose the underlying cause to subscribers
826
877
  .pipe(
827
878
  catchError((err) => {
828
879
  // if query client throws a QueryClientError, extract the cause and throw it
@@ -857,7 +908,9 @@ export class ContextProvider
857
908
  * @returns `true` if the context type is not set or if the item's type ID matches one of the allowed types (case-insensitive); otherwise, `false`.
858
909
  */
859
910
  public validateContext(item: ContextItem<Record<string, unknown>>): boolean {
911
+ // no context type configured means every context item is considered valid
860
912
  if (!this.#contextType) return true;
913
+ // normalize allowed types for a case-insensitive comparison
861
914
  return this.#contextType.map((x) => x.toLowerCase()).includes(item.type.id.toLowerCase());
862
915
  }
863
916
 
@@ -881,7 +934,10 @@ export class ContextProvider
881
934
  // request related context items for the given context item with the same context type which the provider is configured with
882
935
  return this.relatedContexts({ item, filter: { type: this.#contextType } }).pipe(
883
936
  // filter out invalid context items
884
- map((x) => x.filter((item) => this.validateContext(item))),
937
+ map((x) =>
938
+ // keep only context items that validate against the provider's context type
939
+ x.filter((item) => this.validateContext(item)),
940
+ ),
885
941
  map((values) => {
886
942
  // related context should be resolved to a single context item
887
943
  const value = values.shift();
@@ -941,6 +997,7 @@ export class ContextProvider
941
997
  return this.#contextRelated.query(args).pipe(
942
998
  map(({ value }) => value),
943
999
  catchError((err) => {
1000
+ // unwrap the underlying cause so callers see the original error
944
1001
  if (err.cause) {
945
1002
  throw err.cause;
946
1003
  }
@@ -989,7 +1046,12 @@ export default ContextProvider;
989
1046
 
990
1047
  declare module '@equinor/fusion-framework-module-event' {
991
1048
  interface FrameworkEventMap {
992
- // event which is dispatched before the context changes
1049
+ /**
1050
+ * Dispatched **before** the current context is changed.
1051
+ *
1052
+ * The event is cancelable — calling `event.preventDefault()` in a
1053
+ * listener will abort the context change.
1054
+ */
993
1055
  onCurrentContextChange: FrameworkEvent<
994
1056
  FrameworkEventInit<
995
1057
  {
@@ -998,7 +1060,12 @@ declare module '@equinor/fusion-framework-module-event' {
998
1060
  IContextProvider
999
1061
  >
1000
1062
  >;
1001
- // event which is dispatched after the context changes
1063
+ /**
1064
+ * Dispatched **after** the current context has changed.
1065
+ *
1066
+ * Contains both the previous and next context items, enabling
1067
+ * listeners to react to transitions.
1068
+ */
1002
1069
  onCurrentContextChanged: FrameworkEvent<
1003
1070
  FrameworkEventInit<
1004
1071
  {
@@ -1009,7 +1076,12 @@ declare module '@equinor/fusion-framework-module-event' {
1009
1076
  >
1010
1077
  >;
1011
1078
 
1012
- // event which is dispatched before the parent context changes
1079
+ /**
1080
+ * Dispatched **before** a parent context change is applied locally.
1081
+ *
1082
+ * Cancelable — prevents the parent context from being mirrored into
1083
+ * this provider.
1084
+ */
1013
1085
  onParentContextChanged: FrameworkEvent<
1014
1086
  FrameworkEventInit<
1015
1087
  {
@@ -1019,7 +1091,12 @@ declare module '@equinor/fusion-framework-module-event' {
1019
1091
  >
1020
1092
  >;
1021
1093
 
1022
- // event which is dispatched before the context will be resolved
1094
+ /**
1095
+ * Dispatched **before** context resolution begins (when validation
1096
+ * fails and the caller requested resolution).
1097
+ *
1098
+ * Cancelable — aborting prevents the resolution attempt.
1099
+ */
1023
1100
  onSetContextResolve: FrameworkEvent<
1024
1101
  FrameworkEventInit<
1025
1102
  {
@@ -1029,7 +1106,12 @@ declare module '@equinor/fusion-framework-module-event' {
1029
1106
  >
1030
1107
  >;
1031
1108
 
1032
- // event which is dispatched after the context was resolved
1109
+ /**
1110
+ * Dispatched **after** the context has been resolved to a new item.
1111
+ *
1112
+ * Cancelable — aborting prevents the resolved item from being
1113
+ * set as the current context.
1114
+ */
1033
1115
  onSetContextResolved: FrameworkEvent<
1034
1116
  FrameworkEventInit<
1035
1117
  {
@@ -1040,7 +1122,10 @@ declare module '@equinor/fusion-framework-module-event' {
1040
1122
  >
1041
1123
  >;
1042
1124
 
1043
- // event which is dispatched if the context validation failed
1125
+ /**
1126
+ * Dispatched when context validation fails and resolution is not
1127
+ * enabled.
1128
+ */
1044
1129
  onSetContextValidationFailed: FrameworkEvent<
1045
1130
  FrameworkEventInit<
1046
1131
  {
@@ -1050,7 +1135,9 @@ declare module '@equinor/fusion-framework-module-event' {
1050
1135
  >
1051
1136
  >;
1052
1137
 
1053
- // event which is dispatched if the context resolve failed
1138
+ /**
1139
+ * Dispatched when context resolution fails with an error.
1140
+ */
1054
1141
  onSetContextResolveFailed: FrameworkEvent<
1055
1142
  FrameworkEventInit<
1056
1143
  {
@@ -25,7 +25,7 @@ export type GetContextParameters = { id: string };
25
25
  * @template ContextItem The type of the context item managed by the client.
26
26
  * @extends Observable<ContextItem | null | undefined>
27
27
  *
28
- * @todo - should this have `undefined` as a valid value?
28
+ * @todo(#5116) - should this have `undefined` as a valid value?
29
29
  *
30
30
  * @example
31
31
  * ```typescript
@@ -70,6 +70,10 @@ export class ContextClient extends Observable<ContextItem | null | undefined> {
70
70
  return this.#client;
71
71
  }
72
72
 
73
+ /**
74
+ * Creates a new `ContextClient`.
75
+ * @param options - Query constructor options used to fetch a `ContextItem` by its ID.
76
+ */
73
77
  constructor(options: QueryCtorOptions<ContextItem, GetContextParameters>) {
74
78
  super((observer) => this.#currentContext$.subscribe(observer));
75
79
  this.#client = new Query(options);
@@ -85,10 +89,11 @@ export class ContextClient extends Observable<ContextItem | null | undefined> {
85
89
  * @param idOrItem - The context identifier (string), a `ContextItem`, or `null`. If omitted, the current context may be cleared.
86
90
  */
87
91
  public setCurrentContext(idOrItem?: string | ContextItem | null): void {
92
+ // resolve string identifiers to a context item before setting
88
93
  if (typeof idOrItem === 'string') {
89
- // TODO - compare context
94
+ // TODO(#5117) - compare context
95
+ // TODO(#5117) should this catch error?
90
96
  this.resolveContext(idOrItem)
91
- // TODO should this catch error?
92
97
  .pipe(catchError(() => EMPTY))
93
98
  .subscribe((value) => this.setCurrentContext(value));
94
99
  /** only add context if not match */
@@ -105,10 +110,12 @@ export class ContextClient extends Observable<ContextItem | null | undefined> {
105
110
  * @throws Rethrows the underlying error cause if present, otherwise throws the original error.
106
111
  */
107
112
  public resolveContext(id: string): Observable<ContextItem> {
113
+ // unwrap the query result into the resolved context item
108
114
  return this.#client.query({ id }).pipe(
109
115
  map((x) => x.value),
110
116
  // unwrap error
111
117
  catchError((err) => {
118
+ // unwrap the underlying cause so callers see the original error
112
119
  if (err.cause) {
113
120
  throw err.cause;
114
121
  }
@@ -131,6 +138,9 @@ export class ContextClient extends Observable<ContextItem | null | undefined> {
131
138
  return fn(this.resolveContext(id));
132
139
  }
133
140
 
141
+ /**
142
+ * Disposes of the client, completing the internal current-context subject.
143
+ */
134
144
  public dispose(): void {
135
145
  this.#currentContext$.complete();
136
146
  }