@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/CHANGELOG.md CHANGED
@@ -1,14 +1,45 @@
1
1
  # Change Log
2
2
 
3
- ## 7.0.4-next.0
3
+ ## 8.0.1
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - [#3820](https://github.com/equinor/fusion-framework/pull/3820) [`f647825`](https://github.com/equinor/fusion-framework/commit/f647825cb5712763b09dafda21fd996211c78b78) Thanks [@odinr](https://github.com/odinr)! - relase next
7
+ - 80c3e4a: Internal: resolve `noExplicitAny`/`noConfusingVoidType` Biome warnings in `ContextConfigBuilder` and `ContextModuleConfigurator`'s `resolveInitialContext` callback type, using explanatory `biome-ignore` comments for load-bearing `any`/`void` usages. No public API or behavior change.
8
+ - 80c3e4a: Internal: move `FusionContextSearchError` into `src/errors/FusionContextSearchError.ts` with an `errors/index.ts` barrel, resolving a `filename-convention` lint violation; no public API changes.
9
+ - 80c3e4a: Internal: add a required fusion-lint intent comment explaining an `as unknown as T` cast in `ContextProvider`. No behavior change.
10
+ - 80c3e4a: Resolve fusion-lint warnings: add missing TSDoc (constructors, getters, setters, overload implementations, `@throws`/`@template` tags), add intent comments above control-flow and RxJS/iterator chains, and split multi-export files to satisfy `single-export-per-file`.
11
+ - Split `selectors.ts` into `query-context-selector.ts` and `related-context-selector.ts` (re-exported from `selectors.ts`), and extracted `parseContextItem` into `parse-context-item.ts`.
12
+ - Split `extractContextIdFromPath` out of `utils/resolve-context-from-path.ts` into `utils/extract-context-id-from-path.ts` (re-exported from the original file).
13
+ - Deferred work items are now tracked as GitHub issues #5115–#5122.
8
14
 
9
- - Updated dependencies [[`f647825`](https://github.com/equinor/fusion-framework/commit/f647825cb5712763b09dafda21fd996211c78b78)]:
10
- - @equinor/fusion-query@6.0.5-next.0
11
- - @equinor/fusion-framework-module@5.0.7-next.0
15
+ - 80c3e4a: Internal: renamed 45 source files across these packages to comply with the `filename-convention` lint rule (e.g. `AIConfigurator.ts` → `AiConfigurator.ts`, `BookmarkProvider.actions.ts` → `bookmark-actions.ts`, `errors/app-build-error.ts` → `errors/AppBuildError.ts`, `plugins/api/plugin.ts` → `plugins/api/ApiPlugin.ts`, `errors.ts` → `UnsupportedApiVersion.ts`, etc.). Also added `enable-signalr.ts` to the `filename-convention` exclude list since the suggested rename would incorrectly split the "SignalR" brand name. No public API changes.
16
+ - 80c3e4a: Internal: added missing intent comments ahead of non-obvious control flow, RxJS `.pipe()` chains, iterator calls, and multi-source object merges to comply with the `require-intent-comment` and `require-tsdoc` lint rules. Also removed dead duplicate files left over from an earlier refactor in `navigation` (`events.ts`, `navigated-event.ts`, `history.flows.ts` — all fully superseded by their split replacements) and renamed `bookmarks/schemas.ts` to `bookmarks/bookmark.schemas.ts` in `services` to match the `*.schemas.ts` filename convention. No public API changes.
17
+
18
+ ## 8.0.0
19
+
20
+ ### Major Changes
21
+
22
+ - abffa53: Major version bump for Fusion Framework React 19 release.
23
+
24
+ All packages are bumped to the next major version as part of the React 19 upgrade. This release drops support for React versions below 18 and includes breaking changes across the framework.
25
+
26
+ **Breaking changes:**
27
+ - Peer dependencies now require React 18 or 19 (`^18.0.0 || ^19.0.0`)
28
+ - React Router upgraded from v6 to v7
29
+ - Navigation module refactored with new history API
30
+ - `renderComponent` and `renderApp` now use `createRoot` API
31
+
32
+ **Migration:**
33
+ - Update your React version to 18.0.0 or higher before upgrading
34
+ - Replace `NavigationProvider.createRouter()` with `@equinor/fusion-framework-react-router`
35
+ - See individual package changelogs for package-specific migration steps
36
+
37
+ ### Patch Changes
38
+
39
+ - Updated dependencies [abffa53]
40
+ - Updated dependencies [abffa53]
41
+ - @equinor/fusion-framework-module@6.0.0
42
+ - @equinor/fusion-query@7.0.0
12
43
 
13
44
  ## 7.0.3
14
45
 
@@ -20,16 +51,6 @@
20
51
  - @equinor/fusion-framework-module@5.0.6
21
52
  - @equinor/fusion-query@6.0.4
22
53
 
23
- ## 7.0.3-next.0
24
-
25
- ### Patch Changes
26
-
27
- - [#3820](https://github.com/equinor/fusion-framework/pull/3820) [`265bb76`](https://github.com/equinor/fusion-framework/commit/265bb767249989eeb1971e83f3fba94879e0813b) Thanks [@odinr](https://github.com/odinr)! - relase next
28
-
29
- - Updated dependencies [[`265bb76`](https://github.com/equinor/fusion-framework/commit/265bb767249989eeb1971e83f3fba94879e0813b), [`75c068f`](https://github.com/equinor/fusion-framework/commit/75c068fea13c32435ac26bd9043cc156482bfaf1)]:
30
- - @equinor/fusion-query@7.0.0-next.0
31
- - @equinor/fusion-framework-module@5.0.6-next.0
32
-
33
54
  ## 7.0.2
34
55
 
35
56
  ### Patch Changes
package/README.md CHANGED
@@ -1,33 +1,185 @@
1
- ## Overview
1
+ # @equinor/fusion-framework-module-context
2
2
 
3
- The `@equinor/fusion-framework-module-context` package is a core module of the Fusion Framework, designed to manage contextual data within Fusion-based applications and portals.
4
- It provides a structured way to handle context information to enrich user experiences and facilitate data sharing across different components of the application.
3
+ Context module for the Fusion Framework. Manages the active context (project, facility, contract, etc.) within Fusion-based applications and portals, providing query, validation, resolution, and parent–child synchronization out of the box.
5
4
 
6
- ## Configuration
5
+ ## When to use
6
+
7
+ Use this module when your application or portal needs to:
8
+
9
+ - set, read, or clear the active context (e.g. selecting a project)
10
+ - search and filter available context items from the Fusion context API
11
+ - validate whether a given context item matches the allowed context types
12
+ - resolve context across parent–child provider boundaries (portal → app)
13
+ - deep-link by extracting a context ID from the URL path
14
+
15
+ > **Application developers** will typically use the higher-level
16
+ > `@equinor/fusion-framework-react-app/context` package, which wraps this
17
+ > module with React hooks and providers.
18
+ >
19
+ > **Portal developers** will use `@equinor/fusion-framework-react/context`
20
+ > for the same purpose at the portal level.
21
+
22
+ ## Key concepts
23
+
24
+ | Concept | Description |
25
+ |---|---|
26
+ | **ContextItem** | A typed record representing a single context entity (project, facility, etc.). |
27
+ | **ContextProvider** | Runtime service exposing query, set, validate, resolve, and event APIs. |
28
+ | **ContextConfigBuilder** | Fluent builder for configuring context types, filters, clients, and hooks. |
29
+ | **enableContext** | Helper that registers the module on a modules configurator. |
30
+ | **Context resolution** | Automatic lookup of related context items when a context type does not match the configured types. |
31
+ | **Parent connection** | Bi-directional sync between a parent portal context and a child app context. |
32
+
33
+ ## Quick start
7
34
 
8
35
  ```ts
9
36
  import { enableContext } from '@equinor/fusion-framework-module-context';
10
37
 
11
- const configure = (configurator: IModulesConfigurator<any, any>) => {
38
+ export const configure = (configurator) => {
12
39
  enableContext(configurator, (builder) => {
13
- // configure the context module here
40
+ // only accept ProjectMaster context items
41
+ builder.setContextType(['ProjectMaster']);
14
42
  });
15
43
  };
16
44
  ```
17
45
 
18
- ## Features
46
+ Once initialized, access the provider from the module instance:
47
+
48
+ ```ts
49
+ // observe context changes
50
+ modules.context.currentContext$.subscribe((ctx) => {
51
+ console.log('context changed', ctx);
52
+ });
53
+
54
+ // search for context items
55
+ const items = await modules.context.queryContextAsync('Johan');
56
+
57
+ // set context by item
58
+ await modules.context.setCurrentContextAsync(items[0]);
59
+
60
+ // set context by ID
61
+ await modules.context.setCurrentContextByIdAsync('7fd97952-...');
62
+
63
+ // clear the active context
64
+ modules.context.clearCurrentContext();
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ All configuration flows through `enableContext` → `ContextConfigBuilder`:
70
+
71
+ ```ts
72
+ enableContext(configurator, (builder) => {
73
+ // restrict accepted context types
74
+ builder.setContextType(['ProjectMaster', 'Facility']);
75
+
76
+ // post-query filter
77
+ builder.setContextFilter((items) => items.filter((i) => i.isActive));
78
+
79
+ // custom parameter mapping for the search API
80
+ builder.setContextParameterFn(({ search, type }) => ({
81
+ search,
82
+ filter: { type },
83
+ }));
84
+
85
+ // custom validation logic
86
+ builder.setValidateContext(function (item) {
87
+ return item !== null && this.validateContext(item);
88
+ });
89
+
90
+ // custom context resolution
91
+ builder.setResolveContext(function (item) {
92
+ return this.relatedContexts({ item, filter: { type: ['ProjectMaster'] } });
93
+ });
94
+
95
+ // connect (or disconnect) from parent context
96
+ // when enabled (default), onParentContextChanged fires before mirroring the parent's context locally
97
+ builder.connectParentContext(false);
98
+
99
+ // path ↔ context integration
100
+ // the default resolver uses extractContextIdFromPath to pull a GUID from the URL,
101
+ // then fetches the matching context item — see resolveInitialContext in utils
102
+ builder.setContextPathExtractor((path) => path.split('/')[2]);
103
+ builder.setContextPathGenerator((ctx, path) =>
104
+ path.replace(/\/context\/[^/]+/, `/context/${ctx.id}`),
105
+ );
106
+
107
+ // provide a fully custom context client
108
+ builder.setContextClient({
109
+ get: (args) => fetch(`/api/context/${args.id}`).then((r) => r.json()),
110
+ query: (args) => fetch(`/api/context?q=${args.search}`).then((r) => r.json()),
111
+ });
112
+ });
113
+ ```
114
+
115
+ ### Configuration reference
116
+
117
+ | Builder method | Purpose |
118
+ |---|---|
119
+ | `setContextType(types)` | Allowed context type IDs for validation |
120
+ | `setContextFilter(fn)` | Post-query result filter |
121
+ | `setContextParameterFn(fn)` | Maps search + type to API query params |
122
+ | `setValidateContext(fn)` | Custom validation (`this` = provider) |
123
+ | `setResolveContext(fn)` | Custom resolution (`this` = provider) |
124
+ | `connectParentContext(bool)` | Enable/disable parent context sync (fires `onParentContextChanged` on change) |
125
+ | `setContextPathExtractor(fn)` | Extract context ID from URL path |
126
+ | `setContextPathGenerator(fn)` | Generate URL path from context item |
127
+ | `setResolveInitialContext(fn)` | Override initial context resolution (default uses `extractContextIdFromPath` → `resolveContextFromPath`) |
128
+ | `setContextClient(client)` | Custom get/query/related clients |
129
+
130
+ ## Events
131
+
132
+ The context module dispatches events via the `@equinor/fusion-framework-module-event` system. All events are scoped to the `ContextProvider` source.
133
+
134
+ | Event | When | Cancelable |
135
+ |---|---|---|
136
+ | `onCurrentContextChange` | Before the current context is updated | Yes |
137
+ | `onCurrentContextChanged` | After the current context has changed | No |
138
+ | `onParentContextChanged` | Before a parent context change is mirrored locally | Yes |
139
+ | `onSetContextResolve` | Before context resolution begins | Yes |
140
+ | `onSetContextResolved` | After context resolution completes | Yes |
141
+ | `onSetContextValidationFailed` | When validation fails (no resolution) | No |
142
+ | `onSetContextResolveFailed` | When resolution fails with an error | No |
143
+
144
+ ```ts
145
+ modules.event.addEventListener('onCurrentContextChanged', (e) => {
146
+ console.log('previous:', e.detail.previous);
147
+ console.log('next:', e.detail.next);
148
+ });
149
+ ```
150
+
151
+ ## Errors
152
+
153
+ The module exports `FusionContextSearchError` (from `@equinor/fusion-framework-module-context/errors.js`) for search-related failures:
154
+
155
+ ```ts
156
+ import { FusionContextSearchError } from '@equinor/fusion-framework-module-context/errors.js';
157
+
158
+ try {
159
+ await modules.context.queryContextAsync('...');
160
+ } catch (err) {
161
+ if (err instanceof FusionContextSearchError) {
162
+ console.error(err.title, err.description);
163
+ }
164
+ }
165
+ ```
19
166
 
20
- - set and retrieve context items
21
- - query context items based on search strings
22
- - filter context items based on type
23
- - validate context items against allowed types
24
- - resolving related context items
167
+ ## Utilities
25
168
 
26
- ## Usage
169
+ Additional helpers are available from `@equinor/fusion-framework-module-context/utils`:
27
170
 
28
- This module is the core of the context management system in Fusion Framework applications, but there are several packages which has additional functionality or provide a more convenient API for specific use cases.
171
+ | Export | Purpose |
172
+ |---|---|
173
+ | `enableContext` | Register the context module on a configurator |
174
+ | `resolveInitialContext` | Default initial-context resolver (path → parent fallback) |
175
+ | `extractContextIdFromPath` | Extract a GUID-format context ID from a URL path |
176
+ | `resolveContextFromPath` | Create a resolver function that fetches a context item from a URL path |
29
177
 
30
- When developing applications, the `@equinor/fusion-framework-react-app/context` package is typically used to provide a React context provider that integrates with this module.
178
+ ## Package exports
31
179
 
32
- When building portals, the `@equinor/fusion-framework-react/context` package is used to provide a context provider that integrates with this module.
180
+ | Specifier | Description |
181
+ |---|---|
182
+ | `@equinor/fusion-framework-module-context` | Main entry — module, provider, configurator, types |
183
+ | `@equinor/fusion-framework-module-context/errors.js` | `FusionContextSearchError` |
184
+ | `@equinor/fusion-framework-module-context/utils` | Utility functions for path resolution and enablement |
33
185
 
@@ -21,7 +21,7 @@
21
21
  * - The builder pattern allows chaining configuration methods for clarity and convenience.
22
22
  * - The `requireInstance` method enables asynchronous retrieval of module instances by name.
23
23
  *
24
- * @todo - this should extend the BaseConfigBuilder
24
+ * @todo(#5120) - this should extend the BaseConfigBuilder
25
25
  *
26
26
  * @see ContextModuleConfig
27
27
  * @see ModuleInitializerArgs
@@ -29,10 +29,20 @@
29
29
  export class ContextConfigBuilder {
30
30
  config;
31
31
  #init;
32
+ /**
33
+ * Creates a new `ContextConfigBuilder`.
34
+ * @param init - Module initializer arguments used to resolve module instances.
35
+ * @param config - The partial context module config to build upon.
36
+ */
32
37
  constructor(init, config = {}) {
33
38
  this.config = config;
34
39
  this.#init = init;
35
40
  }
41
+ /**
42
+ * Requires a module instance by key or name.
43
+ * @param module - The key or name of the module to resolve.
44
+ * @returns A promise that resolves to the requested module instance.
45
+ */
36
46
  requireInstance(module) {
37
47
  return this.#init.requireInstance(module);
38
48
  }
@@ -101,6 +111,13 @@ export class ContextConfigBuilder {
101
111
  setContextPathGenerator(fn) {
102
112
  this.config.generatePathFromContext = fn;
103
113
  }
114
+ /**
115
+ * Sets the function used to resolve the initial context during module post-initialization.
116
+ *
117
+ * @param fn - A function that returns an observable input emitting the initial context item.
118
+ * The default resolver extracts a context ID from the navigation path, falling
119
+ * back to the parent provider's current context.
120
+ */
104
121
  setResolveInitialContext(fn) {
105
122
  this.config.resolveInitialContext = fn;
106
123
  }
@@ -117,7 +134,7 @@ export class ContextConfigBuilder {
117
134
  * @param expire - Optional. The expiration time (in milliseconds) for cached query results. Defaults to 1 minute.
118
135
  */
119
136
  setContextClient(client, expire = 1 * 60 * 1000) {
120
- this.config.client = {
137
+ const clientConfig = {
121
138
  get: typeof client.get === 'function'
122
139
  ? {
123
140
  key: ({ id }) => id,
@@ -129,7 +146,7 @@ export class ContextConfigBuilder {
129
146
  : client.get,
130
147
  query: typeof client.query === 'function'
131
148
  ? {
132
- // TODO - might cast to checksum
149
+ // TODO(#5118) - might cast to checksum
133
150
  key: (args) => JSON.stringify(args),
134
151
  client: {
135
152
  fn: client.query,
@@ -138,12 +155,13 @@ export class ContextConfigBuilder {
138
155
  }
139
156
  : client.query,
140
157
  };
158
+ this.config.client = clientConfig;
159
+ // only override the related-context client config if one was provided
141
160
  if (client.related) {
142
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
143
- this.config.client.related =
161
+ clientConfig.related =
144
162
  typeof client.related === 'function'
145
163
  ? {
146
- // TODO - might cast to checksum
164
+ // TODO(#5118) - might cast to checksum
147
165
  key: (args) => JSON.stringify(args),
148
166
  client: {
149
167
  fn: client.related,
@@ -1 +1 @@
1
- {"version":3,"file":"ContextConfigBuilder.js","sourceRoot":"","sources":["../../src/ContextConfigBuilder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ContextConfigBuilder.js","sourceRoot":"","sources":["../../src/ContextConfigBuilder.ts"],"names":[],"mappings":"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"}
@@ -44,29 +44,59 @@ export class ContextProvider extends BaseModuleProvider {
44
44
  #contextFilter;
45
45
  #contextParameterFn;
46
46
  #contextQueue = new Subject();
47
+ /**
48
+ * The underlying context client used to resolve and hold the current context item.
49
+ * @returns The internal {@link ContextClient} instance.
50
+ */
47
51
  get contextClient() {
48
52
  return this.#contextClient;
49
53
  }
54
+ /**
55
+ * The query client used to search for context items.
56
+ * @returns The internal `Query` instance used by {@link queryContext}.
57
+ */
50
58
  get queryClient() {
51
59
  return this.#contextQuery;
52
60
  }
61
+ /**
62
+ * Observable stream emitting the current context item.
63
+ * @returns Observable that emits the current `ContextItem`, `null`, or `undefined`.
64
+ */
53
65
  get currentContext$() {
54
66
  return this.#contextClient.currentContext$;
55
67
  }
68
+ /**
69
+ * Snapshot of the current context item.
70
+ * @returns The current `ContextItem`, or `null`/`undefined` if not set.
71
+ */
56
72
  get currentContext() {
57
73
  return this.#contextClient.currentContext;
58
74
  }
59
- /** @deprecated do not use, will be removed */
75
+ /**
76
+ * Sets the current context item.
77
+ * @deprecated do not use, will be removed
78
+ * @param context - The context item to set as current. Must not be `undefined`.
79
+ * @throws Error if `context` is `undefined`.
80
+ */
60
81
  set currentContext(context) {
61
82
  console.warn('@deprecated', 'ContextProvider.currentContext', 'use setCurrentContextById|setCurrentContext|clearCurrentContext');
83
+ // undefined is reserved to mean "not yet initialized", so it cannot be set explicitly
62
84
  if (context === undefined) {
63
85
  throw Error('not allowed to set current context as undefined undefined!');
64
86
  }
65
87
  this.setCurrentContextAsync(context);
66
88
  }
89
+ /**
90
+ * Creates a new instance of `ContextProvider`.
91
+ * @param args - Constructor arguments.
92
+ * @param args.config - The context module configuration.
93
+ * @param args.event - Optional event module instance for dispatching context change events.
94
+ * @param args.parentContext - Optional parent context provider. Deprecated, use {@link connectParentContext}.
95
+ */
67
96
  constructor(args) {
68
97
  const { config, event } = args;
69
98
  super({ version, config });
99
+ // warn about deprecated parentContext constructor arg
70
100
  if (args.parentContext) {
71
101
  console.warn('@deprecated', 'parentContext as arg is deprecated, use ContextProvider.connectParentContext');
72
102
  }
@@ -75,15 +105,18 @@ export class ContextProvider extends BaseModuleProvider {
75
105
  if (config.resolveContext) {
76
106
  this.resolveContext = config.resolveContext?.bind(this);
77
107
  }
108
+ // override validateContext if configured
78
109
  if (config.validateContext) {
79
110
  this.validateContext = config.validateContext?.bind(this);
80
111
  }
112
+ // override extractContextIdFromPath if configured
81
113
  if (config.extractContextIdFromPath) {
82
- // @ts-ignore
114
+ // @ts-expect-error
83
115
  this.extractContextIdFromPath = config.extractContextIdFromPath;
84
116
  }
117
+ // override generatePathFromContext if configured
85
118
  if (config.generatePathFromContext) {
86
- // @ts-ignore
119
+ // @ts-expect-error
87
120
  this.generatePathFromContext = config.generatePathFromContext;
88
121
  }
89
122
  this.#contextType = config.contextType;
@@ -91,6 +124,7 @@ export class ContextProvider extends BaseModuleProvider {
91
124
  // create clients
92
125
  this.#contextClient = new ContextClient(config.client.get);
93
126
  this.#contextQuery = new Query(config.client.query);
127
+ // only create the related-context query when the config provides one
94
128
  if (config.client.related) {
95
129
  this.#contextRelated = new Query(config.client.related);
96
130
  }
@@ -107,9 +141,8 @@ export class ContextProvider extends BaseModuleProvider {
107
141
  this.#subscriptions.add(
108
142
  // observe current context changes
109
143
  this.currentContext$
110
- .pipe(
111
- // emit previous and next context
112
- pairwise())
144
+ // emit previous and next context together for change comparisons
145
+ .pipe(pairwise())
113
146
  .subscribe(([previous, next]) => {
114
147
  this.#event?.dispatchEvent('onCurrentContextChanged', {
115
148
  source: this,
@@ -153,11 +186,12 @@ export class ContextProvider extends BaseModuleProvider {
153
186
  * @returns A `Subscription` object representing the connection to the parent context.
154
187
  */
155
188
  connectParentContext(provider, opt) {
189
+ // build a stream of validated context changes from the parent provider
156
190
  const parentContext$ = provider.currentContext$.pipe(
157
191
  // do not set context if parent has not initialized
158
192
  filter((x) => x !== undefined), filter((next, index) => {
159
193
  // skip first item if opt.skipFirst is true
160
- // TODO: this is a bit hacky, should be handled in a better way
194
+ // TODO(#5121): this is a bit hacky, should be handled in a better way
161
195
  if (opt?.skipFirst && index <= 1) {
162
196
  console.debug('ContextProvider::connectParentContext', 'skipping first item', next);
163
197
  return false;
@@ -165,8 +199,8 @@ export class ContextProvider extends BaseModuleProvider {
165
199
  // only set context if it has changed
166
200
  return this.currentContext?.id !== next?.id;
167
201
  }), switchMap(async (next) => {
202
+ // if parent context is null, just return
168
203
  if (!next) {
169
- // if parent context is null, just return
170
204
  return { next };
171
205
  }
172
206
  // notify event observers that parent context is about to change and await for cancelation
@@ -180,14 +214,16 @@ export class ContextProvider extends BaseModuleProvider {
180
214
  // filter out canceled context changes
181
215
  filter((x) => !x.canceled), switchMap(({ next }) => {
182
216
  // set current context with validation and resolution
183
- return this.setCurrentContext(next, {
217
+ return (this.setCurrentContext(next, {
184
218
  validate: true,
185
219
  resolve: true,
186
- }).pipe(catchError((err) => {
220
+ })
221
+ // swallow errors so a failed context change doesn't break the parent subscription
222
+ .pipe(catchError((err) => {
187
223
  console.warn('ContextProvider::onParentContextChanged', 'setCurrentContext', err);
188
224
  // do not emit any value if an error occurs
189
225
  return EMPTY;
190
- }));
226
+ })));
191
227
  }), catchError((err) => {
192
228
  console.warn('ContextProvider::onParentContextChanged', 'unhandled exception', err);
193
229
  // do not emit any value if an error occurs
@@ -217,6 +253,7 @@ export class ContextProvider extends BaseModuleProvider {
217
253
  this.#contextClient
218
254
  // resolve context item by id
219
255
  .resolveContext(id)
256
+ // filter out invalid items and set as current context
220
257
  .pipe(
221
258
  // filter out invalid context items
222
259
  filter((item) => !!item),
@@ -256,12 +293,18 @@ export class ContextProvider extends BaseModuleProvider {
256
293
  * If the observable is subscribe, unsubscribing __WILL__ abort the task and remove it from queue
257
294
  *
258
295
  * @param context context item which would be queue to set as current
296
+ * @param opt Optional settings.
297
+ * @param opt.validate Whether to validate the context item before setting it.
298
+ * @param opt.resolve Whether to attempt to resolve the context item if validation fails.
299
+ * @template T The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
300
+ * @returns An observable that emits the context item once the queued task completes.
259
301
  */
260
302
  setCurrentContext(context, opt) {
261
303
  // signal for aborting the queue entry
262
304
  const abort$ = new Subject();
263
305
  // wrapper for returning an observable to the caller
264
306
  const subject$ = new Subject();
307
+ // run the actual context-setting logic and relay results/errors to the caller's subject
265
308
  const task$ = this._setCurrentContext(context, opt).pipe(
266
309
  // send context item which was set to the caller
267
310
  tap((x) => subject$.next(x)),
@@ -278,6 +321,7 @@ export class ContextProvider extends BaseModuleProvider {
278
321
  }));
279
322
  // add task to internal queue
280
323
  this.#contextQueue.next(task$);
324
+ // tear down the queued task if the caller unsubscribes
281
325
  return subject$.pipe(
282
326
  // if caller subscribes, unsubscribe should abort queue entry
283
327
  finalize(() => abort$.next(true)));
@@ -311,7 +355,7 @@ export class ContextProvider extends BaseModuleProvider {
311
355
  * - Emit the context and complete the observable.
312
356
  *
313
357
  * @protected
314
- * @typeParam T - The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
358
+ * @template T - The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
315
359
  * @param context - The new context to set.
316
360
  * @param opt - Optional settings:
317
361
  * - `validate`: Whether to validate the context before setting.
@@ -342,7 +386,10 @@ export class ContextProvider extends BaseModuleProvider {
342
386
  // emit error and complete
343
387
  return subscriber.error(Error('failed to validate provided context'));
344
388
  }
389
+ // if resolve is enabled, attempt to resolve the invalid context before setting it
345
390
  if (opt.resolve) {
391
+ // the recursive `_setCurrentContext` call below is re-entered with the already
392
+ // validated/resolved context, so the generic `T` cast is safe here.
346
393
  return of(context)
347
394
  .pipe(
348
395
  // notify event observers that context is about to get resolved
@@ -360,7 +407,9 @@ export class ContextProvider extends BaseModuleProvider {
360
407
  return context;
361
408
  }),
362
409
  // resolve context
363
- switchMap((context) => this.resolveContext(context).pipe(map((resolved) => ({
410
+ switchMap((context) =>
411
+ // Pair the resolved context alongside the original for downstream consumers.
412
+ this.resolveContext(context).pipe(map((resolved) => ({
364
413
  context,
365
414
  resolved,
366
415
  })))),
@@ -413,7 +462,7 @@ export class ContextProvider extends BaseModuleProvider {
413
462
  *
414
463
  * @see {@link setCurrentContext} for more details.
415
464
  *
416
- * @typeParam T - The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
465
+ * @template T - The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
417
466
  * @param context - The context item to set as the current context, or `null` to clear it.
418
467
  * @param opt - Optional settings for context handling.
419
468
  * @param opt.validate - If `true`, validates the context before setting it.
@@ -433,6 +482,7 @@ export class ContextProvider extends BaseModuleProvider {
433
482
  *
434
483
  * @param search - The search string to filter context items.
435
484
  * @returns An Observable that emits an array of `ContextItem` objects matching the search criteria.
485
+ * @throws Re-throws the underlying cause of a `QueryClientError`, otherwise re-throws the original error.
436
486
  */
437
487
  queryContext(search) {
438
488
  const query$ = this.queryClient
@@ -442,6 +492,7 @@ export class ContextProvider extends BaseModuleProvider {
442
492
  search,
443
493
  type: this.#contextType,
444
494
  }))
495
+ // unwrap query-client errors and expose the underlying cause to subscribers
445
496
  .pipe(catchError((err) => {
446
497
  // if query client throws a QueryClientError, extract the cause and throw it
447
498
  if (err.name === 'QueryClientError') {
@@ -470,8 +521,10 @@ export class ContextProvider extends BaseModuleProvider {
470
521
  * @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`.
471
522
  */
472
523
  validateContext(item) {
524
+ // no context type configured means every context item is considered valid
473
525
  if (!this.#contextType)
474
526
  return true;
527
+ // normalize allowed types for a case-insensitive comparison
475
528
  return this.#contextType.map((x) => x.toLowerCase()).includes(item.type.id.toLowerCase());
476
529
  }
477
530
  /**
@@ -492,7 +545,9 @@ export class ContextProvider extends BaseModuleProvider {
492
545
  // request related context items for the given context item with the same context type which the provider is configured with
493
546
  return this.relatedContexts({ item, filter: { type: this.#contextType } }).pipe(
494
547
  // filter out invalid context items
495
- map((x) => x.filter((item) => this.validateContext(item))), map((values) => {
548
+ map((x) =>
549
+ // keep only context items that validate against the provider's context type
550
+ x.filter((item) => this.validateContext(item))), map((values) => {
496
551
  // related context should be resolved to a single context item
497
552
  const value = values.shift();
498
553
  // if no value is found, throw an error
@@ -536,6 +591,7 @@ export class ContextProvider extends BaseModuleProvider {
536
591
  }
537
592
  // request related context items
538
593
  return this.#contextRelated.query(args).pipe(map(({ value }) => value), catchError((err) => {
594
+ // unwrap the underlying cause so callers see the original error
539
595
  if (err.cause) {
540
596
  throw err.cause;
541
597
  }