@equinor/fusion-framework-module-context 3.1.2 → 4.0.0-next.0

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.
@@ -15,6 +15,7 @@ export declare class ContextConfigBuilder<TModules extends Array<AnyModule> = []
15
15
  setContextParameterFn(fn: ContextModuleConfig['contextParameterFn']): void;
16
16
  setValidateContext(fn: ContextModuleConfig['validateContext']): void;
17
17
  setResolveContext(fn: ContextModuleConfig['resolveContext']): void;
18
+ setSkipInitialContext(skip: ContextModuleConfig['skipInitialContext']): void;
18
19
  setContextClient(client: {
19
20
  get: QueryFn<ContextItem, GetContextParameters> | QueryCtorOptions<ContextItem, GetContextParameters>;
20
21
  query: QueryFn<ContextItem[], QueryContextParameters> | QueryCtorOptions<ContextItem[], QueryContextParameters>;
@@ -1,4 +1,4 @@
1
- import { Observable } from 'rxjs';
1
+ import { Observable, Subscription } from 'rxjs';
2
2
  import { ContextModuleConfig } from './configurator';
3
3
  import { ContextClient } from './client/ContextClient';
4
4
  import { ContextItem, QueryContextParameters, RelatedContextParameters } from './types';
@@ -18,6 +18,16 @@ export interface IContextProvider {
18
18
  relatedContexts: (args: RelatedContextParameters) => Observable<Array<ContextItem<Record<string, unknown>>>>;
19
19
  relatedContextsAsync: (args: RelatedContextParameters) => Promise<Array<ContextItem<Record<string, unknown>>>>;
20
20
  clearCurrentContext: VoidFunction;
21
+ setCurrentContextById(id: string): Observable<ContextItem<Record<string, unknown>>>;
22
+ setCurrentContextByIdAsync(id: string): Promise<ContextItem<Record<string, unknown>>>;
23
+ setCurrentContext(context: ContextItem<Record<string, unknown>>, opt?: {
24
+ validate?: boolean;
25
+ resolve?: boolean;
26
+ }): Observable<ContextItem<Record<string, unknown>>>;
27
+ setCurrentContextAsync(context: ContextItem<Record<string, unknown>>, opt?: {
28
+ validate?: boolean;
29
+ resolve?: boolean;
30
+ }): Promise<ContextItem<Record<string, unknown>>>;
21
31
  }
22
32
  export declare class ContextProvider implements IContextProvider {
23
33
  #private;
@@ -31,9 +41,18 @@ export declare class ContextProvider implements IContextProvider {
31
41
  event?: ModuleType<EventModule>;
32
42
  parentContext?: IContextProvider;
33
43
  });
44
+ connectParentContextAsync(provider: IContextProvider, opt?: {
45
+ setCurrent?: boolean;
46
+ }): Promise<Subscription>;
47
+ setCurrentContextById(id: string): Observable<ContextItem<Record<string, unknown>>>;
48
+ setCurrentContextByIdAsync(id: string): Promise<ContextItem<Record<string, unknown>>>;
34
49
  setCurrentContext(context: ContextItem<Record<string, unknown>>, opt?: {
35
50
  validate?: boolean;
36
51
  resolve?: boolean;
52
+ }): Observable<ContextItem<Record<string, unknown>>>;
53
+ setCurrentContextAsync(context: ContextItem<Record<string, unknown>>, opt?: {
54
+ validate?: boolean;
55
+ resolve?: boolean;
37
56
  }): Promise<ContextItem<Record<string, unknown>>>;
38
57
  queryContext(search: string): Observable<Array<ContextItem>>;
39
58
  queryContextAsync(search: string): Promise<Array<ContextItem>>;
@@ -14,5 +14,6 @@ export declare class ContextClient extends Observable<ContextItem> {
14
14
  constructor(options: QueryCtorOptions<ContextItem, GetContextParameters>);
15
15
  setCurrentContext(idOrItem?: string | ContextItem): void;
16
16
  resolveContext(id: string): Observable<ContextItem>;
17
+ dispose(): void;
17
18
  }
18
19
  export default ContextClient;
@@ -13,6 +13,7 @@ export interface ContextModuleConfig {
13
13
  };
14
14
  contextType?: string[];
15
15
  contextFilter?: ContextFilterFn;
16
+ skipInitialContext?: boolean;
16
17
  contextParameterFn?: (args: {
17
18
  search: string;
18
19
  type: ContextModuleConfig['contextType'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equinor/fusion-framework-module-context",
3
- "version": "3.1.2",
3
+ "version": "4.0.0-next.0",
4
4
  "description": "",
5
5
  "main": "./dist/esm/index.js",
6
6
  "exports": {
@@ -44,5 +44,5 @@
44
44
  "@equinor/fusion-framework-module": ">=1.2.5",
45
45
  "rxjs": "^7.5.7"
46
46
  },
47
- "gitHead": "6983a4f2bf1fb24c086abcba53c2349d39f34a21"
47
+ "gitHead": "cdf3599bbf7ed06c1664edd9cb755a6f7b0284dd"
48
48
  }
@@ -65,6 +65,10 @@ export class ContextConfigBuilder<
65
65
  this.config.resolveContext = fn;
66
66
  }
67
67
 
68
+ setSkipInitialContext(skip: ContextModuleConfig['skipInitialContext']) {
69
+ this.config.skipInitialContext = skip;
70
+ }
71
+
68
72
  setContextClient(
69
73
  client: {
70
74
  get:
@@ -1,5 +1,13 @@
1
- import { lastValueFrom, Observable, Subscription, throwError } from 'rxjs';
2
- import { filter, map, pairwise, switchMap } from 'rxjs/operators';
1
+ import {
2
+ EMPTY,
3
+ firstValueFrom,
4
+ lastValueFrom,
5
+ Observable,
6
+ of,
7
+ Subscription,
8
+ throwError,
9
+ } from 'rxjs';
10
+ import { catchError, filter, map, pairwise, switchMap } from 'rxjs/operators';
3
11
 
4
12
  import { ContextModuleConfig } from './configurator';
5
13
 
@@ -37,6 +45,19 @@ export interface IContextProvider {
37
45
  args: RelatedContextParameters
38
46
  ) => Promise<Array<ContextItem<Record<string, unknown>>>>;
39
47
  clearCurrentContext: VoidFunction;
48
+
49
+ setCurrentContextById(id: string): Observable<ContextItem<Record<string, unknown>>>;
50
+ setCurrentContextByIdAsync(id: string): Promise<ContextItem<Record<string, unknown>>>;
51
+
52
+ setCurrentContext(
53
+ context: ContextItem<Record<string, unknown>>,
54
+ opt?: { validate?: boolean; resolve?: boolean }
55
+ ): Observable<ContextItem<Record<string, unknown>>>;
56
+
57
+ setCurrentContextAsync(
58
+ context: ContextItem<Record<string, unknown>>,
59
+ opt?: { validate?: boolean; resolve?: boolean }
60
+ ): Promise<ContextItem<Record<string, unknown>>>;
40
61
  }
41
62
 
42
63
  export class ContextProvider implements IContextProvider {
@@ -68,16 +89,32 @@ export class ContextProvider implements IContextProvider {
68
89
  return this.#contextClient.currentContext;
69
90
  }
70
91
 
92
+ /** @deprecated do not use, will be removed */
71
93
  set currentContext(context: ContextItem | undefined) {
72
- context ? this.setCurrentContext(context) : this.clearCurrentContext();
94
+ console.warn(
95
+ '@deprecated',
96
+ 'ContextProvider.currentContext',
97
+ 'use setCurrentContextById|setCurrentContext|clearCurrentContext'
98
+ );
99
+ context ? this.setCurrentContextAsync(context) : this.clearCurrentContext();
73
100
  }
74
101
 
75
102
  constructor(args: {
76
103
  config: ContextModuleConfig;
77
104
  event?: ModuleType<EventModule>;
105
+ /** @deprecated use ContextProvider.connectParentContext */
78
106
  parentContext?: IContextProvider;
79
107
  }) {
80
- const { config, event, parentContext } = args;
108
+ const { config, event } = args;
109
+
110
+ if (args.parentContext) {
111
+ console.warn(
112
+ '@deprecated',
113
+ 'parentContext as arg is deprecated, use ContextProvider.connectParentContext'
114
+ );
115
+ }
116
+
117
+ this.#event = event;
81
118
 
82
119
  config.resolveContext && (this.resolveContext = config.resolveContext?.bind(this));
83
120
  config.validateContext && (this.validateContext = config.validateContext?.bind(this));
@@ -99,25 +136,21 @@ export class ContextProvider implements IContextProvider {
99
136
  filter: { type: args.type },
100
137
  }));
101
138
 
102
- if (event) {
103
- this.#event = event;
104
-
105
- /** this might be moved to client, to await prevention of event */
139
+ if (this.#event) {
106
140
  this.#subscriptions.add(
107
141
  this.#contextClient.currentContext$
108
142
  .pipe(pairwise())
109
143
  .subscribe(([previous, next]) => {
110
- event.dispatchEvent('onCurrentContextChanged', {
144
+ this.#event?.dispatchEvent('onCurrentContextChanged', {
111
145
  source: this,
112
146
  canBubble: true,
113
147
  detail: { previous, next },
114
148
  });
115
149
  })
116
150
  );
117
-
118
151
  this.#subscriptions.add(
119
152
  /** observe event from child modules */
120
- event.addEventListener('onCurrentContextChanged', (e) => {
153
+ this.#event.addEventListener('onCurrentContextChanged', (e) => {
121
154
  /** loop prevention */
122
155
  if (e.source !== this) {
123
156
  this.currentContext = e.detail.next;
@@ -125,112 +158,195 @@ export class ContextProvider implements IContextProvider {
125
158
  })
126
159
  );
127
160
  }
161
+ }
128
162
 
129
- if (parentContext) {
130
- this.#subscriptions.add(
131
- parentContext.contextClient.currentContext$
132
- .pipe(
133
- switchMap(async (next) => {
134
- if (next) {
135
- const onParentContextChanged = await this.#event?.dispatchEvent(
136
- 'onParentContextChanged',
137
- {
138
- source: this,
139
- detail: next,
140
- cancelable: true,
141
- }
142
- );
143
- return { next, canceled: onParentContextChanged?.canceled };
144
- }
145
- return { next };
146
- }),
147
- filter((x) => !x.canceled),
148
- map(({ next }) => next)
149
- )
150
- .subscribe(async (next) => {
151
- if (next) {
152
- try {
153
- await this.setCurrentContext(next, {
154
- validate: true,
155
- resolve: true,
156
- });
157
- } catch (err) {
158
- console.warn('ContextProvider::onParentContextChanged', err);
159
- }
160
- } else {
161
- this.clearCurrentContext();
163
+ public async connectParentContextAsync(
164
+ provider: IContextProvider,
165
+ opt?: { setCurrent?: boolean }
166
+ ): Promise<Subscription> {
167
+ const parentContext$ = provider.currentContext$.pipe(
168
+ filter((next) => this.currentContext !== next),
169
+ switchMap(async (next) => {
170
+ if (next) {
171
+ const onParentContextChanged = await this.#event?.dispatchEvent(
172
+ 'onParentContextChanged',
173
+ {
174
+ source: this,
175
+ detail: next,
176
+ cancelable: true,
162
177
  }
178
+ );
179
+ return { next, canceled: onParentContextChanged?.canceled };
180
+ }
181
+ return { next };
182
+ }),
183
+ filter((x) => !x.canceled),
184
+ switchMap(({ next }) => {
185
+ if (!next) {
186
+ this.clearCurrentContext();
187
+ return EMPTY;
188
+ }
189
+ return this.setCurrentContext(next, {
190
+ validate: true,
191
+ resolve: true,
192
+ }).pipe(
193
+ catchError((err) => {
194
+ console.warn(
195
+ 'ContextProvider::onParentContextChanged',
196
+ 'setCurrentContext',
197
+ err
198
+ );
199
+ return EMPTY;
163
200
  })
164
- );
165
- }
201
+ );
202
+ }),
203
+ catchError((err) => {
204
+ console.warn('ContextProvider::onParentContextChanged', 'unhandled exception', err);
205
+ return EMPTY;
206
+ })
207
+ );
208
+
209
+ const subscription = parentContext$.subscribe();
210
+ this.#subscriptions.add(subscription);
211
+
212
+ return new Promise((resolve, reject) => {
213
+ if (opt?.setCurrent && provider.currentContext) {
214
+ subscription.add(reject);
215
+ subscription.add(
216
+ this.setCurrentContext(provider.currentContext, {
217
+ validate: true,
218
+ resolve: true,
219
+ }).subscribe({
220
+ error: (err) => {
221
+ subscription.remove(reject);
222
+ reject(err);
223
+ },
224
+ complete: () => {
225
+ subscription.remove(reject);
226
+ resolve(subscription);
227
+ },
228
+ })
229
+ );
230
+ } else {
231
+ return resolve(subscription);
232
+ }
233
+ });
234
+ }
235
+
236
+ public setCurrentContextById(id: string): Observable<ContextItem<Record<string, unknown>>> {
237
+ return new Observable((subscriber) => {
238
+ try {
239
+ this.#contextClient
240
+ .resolveContext(id)
241
+ .pipe(switchMap((item) => this.setCurrentContext(item)))
242
+ .subscribe(subscriber);
243
+ } catch (err) {
244
+ subscriber.error(err);
245
+ }
246
+ });
166
247
  }
167
248
 
168
- public async setCurrentContext(
249
+ public setCurrentContextByIdAsync(id: string): Promise<ContextItem<Record<string, unknown>>> {
250
+ return firstValueFrom(this.setCurrentContextById(id));
251
+ }
252
+
253
+ public setCurrentContext(
169
254
  context: ContextItem<Record<string, unknown>>,
170
255
  opt?: { validate?: boolean; resolve?: boolean }
171
- ): Promise<ContextItem<Record<string, unknown>>> {
172
- if (context === this.currentContext) {
173
- return context;
174
- }
175
- if (opt?.validate && !this.validateContext(context)) {
176
- if (opt.resolve) {
177
- /** notify listeners about to resolve invalid context */
178
- const onSetContextResolve = await this.#event?.dispatchEvent(
179
- 'onSetContextResolve',
180
- {
256
+ ): Observable<ContextItem<Record<string, unknown>>> {
257
+ return new Observable((subscriber) => {
258
+ if (context === this.currentContext) {
259
+ subscriber.next(context);
260
+ return subscriber.complete();
261
+ }
262
+ if (opt?.validate && !this.validateContext(context)) {
263
+ if (!opt.resolve) {
264
+ /** cannot resolve context, and provided is invalid */
265
+ this.#event?.dispatchEvent('onSetContextValidationFailed', {
181
266
  source: this,
182
- cancelable: true,
183
267
  detail: { context },
184
- }
185
- );
186
- if (onSetContextResolve?.canceled) {
187
- throw Error('resolving of context was canceled');
268
+ });
269
+ return subscriber.error(Error('failed to validate provided context'));
188
270
  }
271
+ if (opt.resolve) {
272
+ return of(context)
273
+ .pipe(
274
+ /** notify event observers that context is about to get resolved */
275
+ switchMap(async (context) => {
276
+ const event = await this.#event?.dispatchEvent(
277
+ 'onSetContextResolve',
278
+ {
279
+ source: this,
280
+ cancelable: true,
281
+ detail: { context },
282
+ }
283
+ );
284
+ if (event?.canceled) {
285
+ throw Error('resolving of context was canceled');
286
+ }
287
+ return context;
288
+ }),
289
+ /** execute resolve */
290
+ switchMap((context) =>
291
+ this.resolveContext(context).pipe(
292
+ map((resolved) => ({
293
+ context,
294
+ resolved,
295
+ }))
296
+ )
297
+ ),
298
+ /** notify event observers that context was resolved */
299
+ switchMap(async ({ context, resolved }) => {
300
+ const event = await this.#event?.dispatchEvent(
301
+ 'onSetContextResolved',
302
+ {
303
+ source: this,
304
+ cancelable: true,
305
+ detail: { context, resolved },
306
+ }
307
+ );
308
+ if (event?.canceled) {
309
+ throw Error('resolving of context was canceled');
310
+ }
311
+ return resolved;
312
+ }),
313
+ /** recursive set current context without validation and resolve */
314
+ switchMap((resolved) => this.setCurrentContext(resolved))
315
+ )
316
+ .subscribe(subscriber);
317
+ }
318
+ }
189
319
 
190
- try {
191
- const resolved = await this.resolveContextAsync(context);
192
- /** notify listeners about to resolved invalid context */
193
- const onSetContextResolved = await this.#event?.dispatchEvent(
194
- 'onSetContextResolved',
195
- {
320
+ return of(context)
321
+ .pipe(
322
+ switchMap(async (context) => {
323
+ const event = await this.#event?.dispatchEvent('onCurrentContextChange', {
196
324
  source: this,
325
+ canBubble: true,
197
326
  cancelable: true,
198
- detail: { context, resolved },
327
+ detail: { context: context },
328
+ });
329
+
330
+ if (event?.canceled) {
331
+ throw Error('change of context was aborted');
199
332
  }
200
- );
201
- if (onSetContextResolved?.canceled) {
202
- throw Error('resolving of context was canceled');
203
- }
204
- return this.setCurrentContext(resolved);
205
- } catch (error) {
206
- console.error('failed to resolve context', context, error);
207
- this.#event?.dispatchEvent('onSetContextResolveFailed', {
208
- source: this,
209
- detail: { context, error },
210
- });
211
- }
212
- }
213
- this.#event?.dispatchEvent('onSetContextValidationFailed', {
214
- source: this,
215
- detail: { context },
216
- });
217
- throw Error('failed to validate provided context');
218
- }
219
333
 
220
- const onCurrentContextChange = await this.#event?.dispatchEvent('onCurrentContextChange', {
221
- source: this,
222
- canBubble: true,
223
- cancelable: true,
224
- detail: { context: context },
334
+ return context;
335
+ })
336
+ )
337
+ .subscribe((context) => {
338
+ this.#contextClient.setCurrentContext(context);
339
+ subscriber.next(context);
340
+ subscriber.complete();
341
+ });
225
342
  });
343
+ }
226
344
 
227
- if (onCurrentContextChange?.canceled) {
228
- throw Error('change of context was aborted');
229
- }
230
-
231
- this.#contextClient.setCurrentContext(context);
232
-
233
- return context;
345
+ public async setCurrentContextAsync(
346
+ context: ContextItem<Record<string, unknown>>,
347
+ opt?: { validate?: boolean; resolve?: boolean }
348
+ ): Promise<ContextItem<Record<string, unknown>>> {
349
+ return firstValueFrom(this.setCurrentContext(context, opt));
234
350
  }
235
351
 
236
352
  public queryContext(search: string): Observable<Array<ContextItem>> {
@@ -308,6 +424,7 @@ export class ContextProvider implements IContextProvider {
308
424
 
309
425
  dispose() {
310
426
  this.#subscriptions.unsubscribe();
427
+ this.#contextClient.dispose();
311
428
  }
312
429
  }
313
430
 
@@ -47,6 +47,10 @@ export class ContextClient extends Observable<ContextItem> {
47
47
  public resolveContext(id: string): Observable<ContextItem> {
48
48
  return this.#client.query({ id }).pipe(map((x) => x.value));
49
49
  }
50
+
51
+ public dispose(): void {
52
+ this.#currentContext$.complete();
53
+ }
50
54
  }
51
55
 
52
56
  export default ContextClient;
@@ -21,6 +21,9 @@ export interface ContextModuleConfig {
21
21
  contextType?: string[];
22
22
  contextFilter?: ContextFilterFn;
23
23
 
24
+ /** set initial context from parent, will await resolve */
25
+ skipInitialContext?: boolean;
26
+
24
27
  /**
25
28
  * Method for generating context query parameters.
26
29
  */
package/src/module.ts CHANGED
@@ -23,8 +23,21 @@ export const module: ContextModule = {
23
23
  initialize: async (args) => {
24
24
  const config = await (args.config as ContextModuleConfigurator).createConfig(args);
25
25
  const event = args.hasModule('event') ? await args.requireInstance('event') : undefined;
26
- const parentContext = (args.ref as ModulesInstance<[ContextModule]>)?.context;
27
- return new ContextProvider({ config, event, parentContext });
26
+ const parentProvider = (args.ref as ModulesInstance<[ContextModule]>)?.context;
27
+ const provider = new ContextProvider({ config, event, parentContext: parentProvider });
28
+
29
+ // TODO add option for skipping this step
30
+ if (parentProvider) {
31
+ try {
32
+ await provider.connectParentContextAsync(parentProvider, {
33
+ setCurrent: !config.skipInitialContext,
34
+ });
35
+ } catch (err) {
36
+ console.warn('provider.connectParentContext', 'failed to set parent context', err);
37
+ }
38
+ }
39
+
40
+ return provider;
28
41
  },
29
42
 
30
43
  dispose: (args) => {