@equinor/fusion-framework-module-app 8.0.1 → 8.0.3

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 (109) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/esm/AppClient.js +75 -8
  3. package/dist/esm/AppClient.js.map +1 -1
  4. package/dist/esm/AppConfig.js +9 -1
  5. package/dist/esm/AppConfig.js.map +1 -1
  6. package/dist/esm/{AppClient.Selectors.js → AppConfigSelector.js} +1 -1
  7. package/dist/esm/AppConfigSelector.js.map +1 -0
  8. package/dist/esm/AppConfigurator.js +28 -2
  9. package/dist/esm/AppConfigurator.js.map +1 -1
  10. package/dist/esm/AppModuleProvider.js +16 -0
  11. package/dist/esm/AppModuleProvider.js.map +1 -1
  12. package/dist/esm/app/App.js +82 -24
  13. package/dist/esm/app/App.js.map +1 -1
  14. package/dist/esm/app/actions.js +2 -2
  15. package/dist/esm/app/actions.js.map +1 -1
  16. package/dist/esm/app/create-reducer.js +2 -1
  17. package/dist/esm/app/create-reducer.js.map +1 -1
  18. package/dist/esm/app/filter-empty.js +11 -0
  19. package/dist/esm/app/filter-empty.js.map +1 -0
  20. package/dist/esm/app/flows/handle-fetch-config.js +30 -0
  21. package/dist/esm/app/flows/handle-fetch-config.js.map +1 -0
  22. package/dist/esm/app/flows/handle-fetch-manifest.js +30 -0
  23. package/dist/esm/app/flows/handle-fetch-manifest.js.map +1 -0
  24. package/dist/esm/app/flows/handle-fetch-settings.js +30 -0
  25. package/dist/esm/app/flows/handle-fetch-settings.js.map +1 -0
  26. package/dist/esm/app/flows/handle-import-application.js +23 -0
  27. package/dist/esm/app/flows/handle-import-application.js.map +1 -0
  28. package/dist/esm/app/flows/handle-update-settings.js +22 -0
  29. package/dist/esm/app/flows/handle-update-settings.js.map +1 -0
  30. package/dist/esm/app/flows/index.js +6 -0
  31. package/dist/esm/app/flows/index.js.map +1 -0
  32. package/dist/esm/enable-app-module.js +2 -1
  33. package/dist/esm/enable-app-module.js.map +1 -1
  34. package/dist/esm/errors/AppBuildError.js +35 -0
  35. package/dist/esm/errors/AppBuildError.js.map +1 -0
  36. package/dist/esm/errors/AppConfigError.js +35 -0
  37. package/dist/esm/errors/AppConfigError.js.map +1 -0
  38. package/dist/esm/errors/AppManifestError.js +35 -0
  39. package/dist/esm/errors/AppManifestError.js.map +1 -0
  40. package/dist/esm/errors/AppScriptModuleError.js +17 -0
  41. package/dist/esm/errors/AppScriptModuleError.js.map +1 -0
  42. package/dist/esm/errors/AppSettingsError.js +35 -0
  43. package/dist/esm/errors/AppSettingsError.js.map +1 -0
  44. package/dist/esm/errors/app-error-type.js +2 -0
  45. package/dist/esm/errors/app-error-type.js.map +1 -0
  46. package/dist/esm/errors.js +5 -148
  47. package/dist/esm/errors.js.map +1 -1
  48. package/dist/esm/module.js +2 -0
  49. package/dist/esm/module.js.map +1 -1
  50. package/dist/esm/schemas.js +4 -0
  51. package/dist/esm/schemas.js.map +1 -1
  52. package/dist/esm/version.js +1 -1
  53. package/dist/tsconfig.tsbuildinfo +1 -1
  54. package/dist/types/AppClient.d.ts +42 -0
  55. package/dist/types/AppConfig.d.ts +7 -1
  56. package/dist/types/AppConfigurator.d.ts +25 -0
  57. package/dist/types/AppModuleProvider.d.ts +10 -0
  58. package/dist/types/app/App.d.ts +49 -8
  59. package/dist/types/app/create-reducer.d.ts +55 -55
  60. package/dist/types/app/filter-empty.d.ts +8 -0
  61. package/dist/types/app/flows/handle-fetch-config.d.ts +12 -0
  62. package/dist/types/app/flows/handle-fetch-manifest.d.ts +12 -0
  63. package/dist/types/app/flows/handle-fetch-settings.d.ts +12 -0
  64. package/dist/types/app/flows/handle-import-application.d.ts +10 -0
  65. package/dist/types/app/flows/handle-update-settings.d.ts +12 -0
  66. package/dist/types/app/flows/index.d.ts +5 -0
  67. package/dist/types/errors/AppBuildError.d.ts +21 -0
  68. package/dist/types/errors/AppConfigError.d.ts +21 -0
  69. package/dist/types/errors/AppManifestError.d.ts +21 -0
  70. package/dist/types/errors/AppScriptModuleError.d.ts +14 -0
  71. package/dist/types/errors/AppSettingsError.d.ts +21 -0
  72. package/dist/types/errors/app-error-type.d.ts +9 -0
  73. package/dist/types/errors.d.ts +6 -103
  74. package/dist/types/version.d.ts +1 -1
  75. package/package.json +10 -10
  76. package/src/AppClient.ts +65 -4
  77. package/src/AppConfig.ts +9 -1
  78. package/src/AppConfigurator.ts +28 -4
  79. package/src/AppModuleProvider.ts +16 -0
  80. package/src/app/App.ts +88 -31
  81. package/src/app/actions.ts +2 -2
  82. package/src/app/create-reducer.ts +2 -1
  83. package/src/app/filter-empty.ts +11 -0
  84. package/src/app/flows/handle-fetch-config.ts +48 -0
  85. package/src/app/flows/handle-fetch-manifest.ts +53 -0
  86. package/src/app/flows/handle-fetch-settings.ts +50 -0
  87. package/src/app/flows/handle-import-application.ts +37 -0
  88. package/src/app/flows/handle-update-settings.ts +39 -0
  89. package/src/app/flows/index.ts +5 -0
  90. package/src/app/types.ts +2 -2
  91. package/src/enable-app-module.ts +2 -1
  92. package/src/errors/AppBuildError.ts +47 -0
  93. package/src/errors/AppConfigError.ts +47 -0
  94. package/src/errors/AppManifestError.ts +47 -0
  95. package/src/errors/AppScriptModuleError.ts +20 -0
  96. package/src/errors/AppSettingsError.ts +47 -0
  97. package/src/errors/app-error-type.ts +9 -0
  98. package/src/errors.ts +6 -208
  99. package/src/module.ts +2 -0
  100. package/src/schemas.ts +4 -0
  101. package/src/types.ts +1 -1
  102. package/src/version.ts +1 -1
  103. package/dist/esm/AppClient.Selectors.js.map +0 -1
  104. package/dist/esm/app/flows.js +0 -120
  105. package/dist/esm/app/flows.js.map +0 -1
  106. package/dist/types/app/flows.d.ts +0 -41
  107. package/src/app/flows.ts +0 -189
  108. /package/dist/types/{AppClient.Selectors.d.ts → AppConfigSelector.d.ts} +0 -0
  109. /package/src/{AppClient.Selectors.ts → AppConfigSelector.ts} +0 -0
package/src/app/App.ts CHANGED
@@ -9,15 +9,8 @@ import type {
9
9
  import { type FlowSubject, Observable } from '@equinor/fusion-observable';
10
10
 
11
11
  import type { AppModuleProvider } from '../AppModuleProvider';
12
- import {
13
- combineLatest,
14
- of,
15
- type OperatorFunction,
16
- Subscription,
17
- firstValueFrom,
18
- lastValueFrom,
19
- } from 'rxjs';
20
- import { defaultIfEmpty, filter, last, map, switchMap } from 'rxjs/operators';
12
+ import { combineLatest, of, Subscription, firstValueFrom, lastValueFrom } from 'rxjs';
13
+ import { defaultIfEmpty, last, map, switchMap } from 'rxjs/operators';
21
14
 
22
15
  import type { EventModule } from '@equinor/fusion-framework-module-event';
23
16
  import type { AnyModule, ModuleType } from '@equinor/fusion-framework-module';
@@ -27,17 +20,11 @@ import type { AppBundleState, AppBundleStateInitial } from './types';
27
20
 
28
21
  import isEqual from 'fast-deep-equal';
29
22
 
23
+ import { filterEmpty } from './filter-empty';
24
+
30
25
  import './events';
31
26
 
32
- /**
33
- * RxJS operator that filters out `null` and `undefined` emissions.
34
- *
35
- * @template T - The non-nullable value type.
36
- * @returns An operator that only passes through non-nullable values.
37
- */
38
- export function filterEmpty<T>(): OperatorFunction<T | null | undefined, T> {
39
- return filter((value): value is T => value !== undefined && value !== null);
40
- }
27
+ export { filterEmpty } from './filter-empty';
41
28
 
42
29
  /**
43
30
  * Public interface for a single loaded Fusion application.
@@ -269,6 +256,7 @@ export type AppInitializeResult = {
269
256
  config: AppConfig;
270
257
  };
271
258
 
259
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
272
260
  /**
273
261
  * Concrete implementation of {@link IApp}.
274
262
  *
@@ -276,11 +264,11 @@ export type AppInitializeResult = {
276
264
  * manifest fetching, config loading, settings management, and script import. Dispatches
277
265
  * lifecycle events through the {@link EventModule} when available.
278
266
  *
267
+ * TODO(#5130): streams should be made distinct until changed from state.
268
+ *
279
269
  * @template TEnv - Shape of the environment configuration record.
280
270
  * @template TModules - Additional framework modules the app depends on.
281
271
  */
282
- // TODO make streams distinct until changed from state
283
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
284
272
  export class App<
285
273
  TEnv extends ConfigEnvironment = ConfigEnvironment,
286
274
  TModules extends Array<AnyModule> | unknown = unknown,
@@ -290,76 +278,110 @@ export class App<
290
278
 
291
279
  //#region === streams ===
292
280
 
281
+ /** @inheritdoc */
293
282
  get manifest$(): Observable<AppManifest> {
283
+ // filter out null/undefined manifest states
294
284
  return this.#state.select((state) => state.manifest).pipe(filterEmpty());
295
285
  }
296
286
 
287
+ /** @inheritdoc */
297
288
  get config$(): Observable<AppConfig<TEnv>> {
298
- return this.#state
299
- .select((state) => state.config as AppConfig<TEnv>, isEqual)
300
- .pipe(filterEmpty());
289
+ return (
290
+ this.#state
291
+ .select((state) => state.config as AppConfig<TEnv>, isEqual)
292
+ // filter out null/undefined config states
293
+ .pipe(filterEmpty())
294
+ );
301
295
  }
302
296
 
297
+ /** @inheritdoc */
303
298
  get modules$(): Observable<AppScriptModule> {
299
+ // filter out null/undefined module states
304
300
  return this.#state.select((state) => state.modules).pipe(filterEmpty());
305
301
  }
306
302
 
303
+ /** @inheritdoc */
307
304
  get instance$(): Observable<AppModulesInstance<TModules>> {
308
- return this.#state
309
- .select((state) => state.instance as AppModulesInstance<TModules>)
310
- .pipe(filterEmpty());
305
+ return (
306
+ this.#state
307
+ .select((state) => state.instance as AppModulesInstance<TModules>)
308
+ // filter out null/undefined instance states
309
+ .pipe(filterEmpty())
310
+ );
311
311
  }
312
312
 
313
+ /** @inheritdoc */
313
314
  get settings$(): Observable<AppSettings> {
314
315
  return new Observable<AppSettings>((subscriber) => {
315
316
  this.#state.next(actions.fetchSettings(this.appKey));
316
317
  subscriber.add(
317
318
  this.#state
318
319
  .select((state) => state.settings, isEqual)
320
+ // filter out null/undefined settings and fall back to the default empty settings
319
321
  .pipe(filterEmpty(), defaultIfEmpty(fallbackSettings))
320
322
  .subscribe(subscriber),
321
323
  );
322
324
  });
323
325
  }
324
326
 
327
+ /** @inheritdoc */
325
328
  get status$(): Observable<AppBundleState['status']> {
326
329
  return this.#state.select((state) => state.status);
327
330
  }
328
331
 
329
332
  //#endregion
330
333
 
334
+ /** @inheritdoc */
331
335
  get state(): Readonly<AppBundleState> {
332
- // todo deep-freeze
336
+ // TODO(#5133) deep-freeze
333
337
  return Object.freeze(this.#state.value) as Readonly<AppBundleState>;
334
338
  }
335
339
 
340
+ /** @inheritdoc */
336
341
  get appKey(): string {
337
342
  return this.#state.value.appKey;
338
343
  }
344
+ /**
345
+ * The tag (version) this app instance was loaded with, if any.
346
+ * @returns The tag string, or `undefined` if none was specified.
347
+ */
339
348
  get tag(): string | undefined {
340
349
  return this.#state.value.tag;
341
350
  }
342
351
 
352
+ /** @inheritdoc */
343
353
  get manifest(): Readonly<AppManifest> | undefined {
344
354
  return this.state.manifest;
345
355
  }
346
356
 
357
+ /** @inheritdoc */
347
358
  get manifestAsync(): Promise<Readonly<AppManifest>> {
348
359
  return firstValueFrom(this.manifest$);
349
360
  }
350
361
 
362
+ /** @inheritdoc */
351
363
  get config(): AppConfig<TEnv> | undefined {
352
364
  return this.state.config as AppConfig<TEnv>;
353
365
  }
354
366
 
367
+ /**
368
+ * Retrieves the configuration asynchronously.
369
+ * @returns A promise that resolves to the {@link AppConfig}.
370
+ */
355
371
  get configAsync(): Promise<AppConfig<TEnv>> {
356
372
  return firstValueFrom(this.config$);
357
373
  }
358
374
 
375
+ /** @inheritdoc */
359
376
  get instance(): AppModulesInstance<TModules> | undefined {
360
377
  return this.#state.value.instance as AppModulesInstance<TModules>;
361
378
  }
362
379
 
380
+ /**
381
+ * Creates a new {@link App} instance and starts its internal state machine.
382
+ * @param value - Initial app bundle state (appKey, tag, and any pre-loaded data).
383
+ * @param args - The owning {@link AppModuleProvider} and an optional event module for lifecycle events.
384
+ */
363
385
  constructor(
364
386
  value: AppBundleStateInitial,
365
387
  args: {
@@ -378,6 +400,7 @@ export class App<
378
400
  // create a tear down handler for the application
379
401
  const subscriptions = new Subscription();
380
402
 
403
+ // only wire up event listeners when an event module is provided
381
404
  if (event) {
382
405
  // when app is disposed, dispatch event to notify listeners
383
406
  subscriptions.add(() => {
@@ -398,6 +421,7 @@ export class App<
398
421
 
399
422
  this.dispose = () => {
400
423
  subscriptions?.unsubscribe();
424
+ // tear down modules of application, if an instance was loaded
401
425
  if (this.#state.value.instance) {
402
426
  // tear down modules of application
403
427
  this.#state.value.instance.dispose();
@@ -576,6 +600,7 @@ export class App<
576
600
  });
577
601
  }
578
602
 
603
+ /** @inheritdoc */
579
604
  public initialize(): Observable<AppInitializeResult> {
580
605
  return new Observable((subscriber) => {
581
606
  // dispatch initialize action to indicate that the application is initializing
@@ -605,8 +630,9 @@ export class App<
605
630
  });
606
631
  }
607
632
 
633
+ /** @inheritdoc */
608
634
  public loadConfig() {
609
- // TODO - shit fix
635
+ // TODO(#5126) - shit fix
610
636
  (this.manifest ? of(this.manifest) : this.getManifest()).subscribe({
611
637
  next: (manifest) => {
612
638
  this.#state.next(actions.fetchConfig(manifest));
@@ -614,16 +640,24 @@ export class App<
614
640
  });
615
641
  }
616
642
 
643
+ /** @inheritdoc */
617
644
  public loadManifest(update?: boolean) {
618
645
  this.#state.next(actions.fetchManifest(this.appKey, this.tag, update));
619
646
  }
620
647
 
648
+ /**
649
+ * Replaces or merges the current manifest with a new one.
650
+ * @param manifest - The manifest to apply.
651
+ * @param replace - When `false` (default), merges into the existing manifest; when omitted, behaves the same.
652
+ */
621
653
  public updateManifest(manifest: AppManifest, replace?: false) {
622
654
  this.#state.next(actions.setManifest(manifest, !replace));
623
655
  }
624
656
 
657
+ /** @inheritdoc */
625
658
  public async loadAppModule(allow_cache = true) {
626
659
  const manifest = await this.getManifestAsync(allow_cache);
660
+ // only import the app module if the manifest declares an entry point
627
661
  if (manifest.build?.entryPoint) {
628
662
  this.#state.next(actions.importApp(manifest.build.entryPoint));
629
663
  } else {
@@ -633,11 +667,14 @@ export class App<
633
667
  }
634
668
  }
635
669
 
670
+ /** @inheritdoc */
636
671
  public getConfig(force_refresh = false): Observable<AppConfig> {
637
672
  return new Observable((subscriber) => {
673
+ // emit the cached config immediately, if one is already loaded
638
674
  if (this.#state.value.config) {
639
675
  // emit current config to the subscriber
640
676
  subscriber.next(this.#state.value.config);
677
+ // complete the stream unless a forced refresh was requested
641
678
  if (!force_refresh) {
642
679
  // since we have the config and no force refresh, complete the stream
643
680
  return subscriber.complete();
@@ -679,17 +716,21 @@ export class App<
679
716
  });
680
717
  }
681
718
 
719
+ /** @inheritdoc */
682
720
  public getConfigAsync(allow_cache = true): Promise<AppConfig> {
683
721
  // when allow_cache is true, use first emitted value, otherwise use last emitted value
684
722
  const operator = allow_cache ? firstValueFrom : lastValueFrom;
685
723
  return operator(this.getConfig(!allow_cache));
686
724
  }
687
725
 
726
+ /** @inheritdoc */
688
727
  public getSettings<T extends AppSettings>(force_refresh = false): Observable<T> {
689
728
  return new Observable<T>((subscriber) => {
729
+ // emit the cached settings immediately, if already loaded
690
730
  if (this.#state.value.settings) {
691
731
  // emit current settings to the subscriber
692
732
  subscriber.next(this.#state.value.settings as T);
733
+ // complete the stream unless a forced refresh was requested
693
734
  if (!force_refresh) {
694
735
  // since we have the settings and no force refresh, complete the stream
695
736
  return subscriber.complete();
@@ -731,12 +772,14 @@ export class App<
731
772
  });
732
773
  }
733
774
 
775
+ /** @inheritdoc */
734
776
  public getSettingsAsync<T extends AppSettings>(allow_cache = true): Promise<T> {
735
777
  // when allow_cache is true, use first emitted value, otherwise use last emitted value
736
778
  const operator = allow_cache ? firstValueFrom : lastValueFrom;
737
779
  return operator(this.getSettings<T>(!allow_cache));
738
780
  }
739
781
 
782
+ /** @inheritdoc */
740
783
  public updateSettings<T extends AppSettings>(settings: T): Observable<T> {
741
784
  return new Observable((subscriber) => {
742
785
  subscriber.add(
@@ -770,10 +813,12 @@ export class App<
770
813
  });
771
814
  }
772
815
 
816
+ /** @inheritdoc */
773
817
  public updateSettingsAsync<T extends AppSettings>(settings: T): Promise<T> {
774
818
  return lastValueFrom(this.updateSettings(settings));
775
819
  }
776
820
 
821
+ /** @inheritdoc */
777
822
  public updateSetting<T extends AppSettings, P extends keyof T>(
778
823
  property: P,
779
824
  value: T[P],
@@ -781,12 +826,14 @@ export class App<
781
826
  const currentSettings$ =
782
827
  this.#state.value.settings === undefined
783
828
  ? // if settings are not loaded, fetch settings
784
- this.getSettings().pipe(last())
829
+ this.getSettings()
830
+ // take the last emitted value
831
+ .pipe(last())
785
832
  : // if settings are loaded, use current settings
786
833
  of(this.#state.value.settings);
787
834
 
835
+ // merge the current settings with the new value, then persist and return the updated property
788
836
  return currentSettings$.pipe(
789
- // merge current settings with new value
790
837
  map((settings) => ({ ...settings, [property]: value })),
791
838
  // update settings
792
839
  switchMap((settings) => this.updateSettings<T>(settings as T)),
@@ -795,6 +842,7 @@ export class App<
795
842
  );
796
843
  }
797
844
 
845
+ /** @inheritdoc */
798
846
  public updateSettingAsync<T extends AppSettings, P extends keyof T>(
799
847
  property: P,
800
848
  value: T[P],
@@ -802,11 +850,14 @@ export class App<
802
850
  return lastValueFrom(this.updateSetting<T, P>(property, value));
803
851
  }
804
852
 
853
+ /** @inheritdoc */
805
854
  public getManifest(force_refresh = false): Observable<AppManifest> {
806
855
  return new Observable((subscriber) => {
856
+ // emit the cached manifest immediately, if already loaded
807
857
  if (this.#state.value.manifest) {
808
858
  // emit current manifest to the subscriber
809
859
  subscriber.next(this.#state.value.manifest);
860
+ // complete the stream unless a forced refresh was requested
810
861
  if (!force_refresh) {
811
862
  // since we have the manifest and no force refresh, complete the stream
812
863
  return subscriber.complete();
@@ -846,17 +897,21 @@ export class App<
846
897
  });
847
898
  }
848
899
 
900
+ /** @inheritdoc */
849
901
  public getManifestAsync(allow_cache = true): Promise<AppManifest> {
850
902
  // when allow_cache is true, use first emitted value, otherwise use last emitted value
851
903
  const operator = allow_cache ? firstValueFrom : lastValueFrom;
852
904
  return operator(this.getManifest(!allow_cache));
853
905
  }
854
906
 
907
+ /** @inheritdoc */
855
908
  public getAppModule(force_refresh = false): Observable<AppScriptModule> {
856
909
  return new Observable((subscriber) => {
910
+ // emit the cached module immediately, if already loaded
857
911
  if (this.#state.value.modules) {
858
912
  // emit current value to the subscriber
859
913
  subscriber.next(this.#state.value.modules);
914
+ // complete the stream unless a forced refresh was requested
860
915
  if (!force_refresh) {
861
916
  // complete if no force refresh
862
917
  return subscriber.complete();
@@ -898,8 +953,9 @@ export class App<
898
953
  subscriber.add(
899
954
  // fetch application latest manifest and request loading of the application script
900
955
  this.getManifest().subscribe((manifest) => {
956
+ // only import the app module if the manifest declares an entry point
901
957
  if (manifest.build?.entryPoint) {
902
- // TODO - this should come from backend
958
+ // TODO(#5128) - this should come from backend
903
959
  const assetPath =
904
960
  manifest.build.assetPath ?? [manifest.appKey, manifest.build.version].join('@');
905
961
  // dispatch import_app action to load the application script
@@ -914,6 +970,7 @@ export class App<
914
970
  });
915
971
  }
916
972
 
973
+ /** @inheritdoc */
917
974
  public getAppModuleAsync(allow_cache = true): Promise<AppScriptModule> {
918
975
  // when allow_cache is true, use first emitted value, otherwise use last emitted value
919
976
  const operator = allow_cache ? firstValueFrom : lastValueFrom;
@@ -29,7 +29,7 @@ const createActions = () => ({
29
29
  setManifest: createAction('set_manifest', (manifest: AppManifest, update?: boolean) => ({
30
30
  payload: manifest,
31
31
  meta: {
32
- // TODO when updating
32
+ // TODO(#5129) when updating
33
33
  created: Date.now(),
34
34
  update,
35
35
  },
@@ -77,7 +77,7 @@ const createActions = () => ({
77
77
  payload: id,
78
78
  })),
79
79
  /** App loading */
80
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
+ // biome-ignore lint/suspicious/noExplicitAny: module payload widens to accept any script module shape when set
81
81
  setModule: createAction('set_module', (module: any) => ({ payload: module })),
82
82
  importApp: createAsyncAction(
83
83
  'import_app',
@@ -29,8 +29,9 @@ export const createReducer = (value: AppBundleStateInitial) =>
29
29
  builder
30
30
  // update or set manifest
31
31
  .addCase(actions.setManifest, (state, action) => {
32
- // TODO: after legacy is removed, remove the update flag
32
+ // TODO(#5131): after legacy is removed, remove the update flag
33
33
  if (action.meta.update) {
34
+ // Merge onto the existing manifest so partial updates don't clobber other fields.
34
35
  state.manifest = Object.assign(state.manifest ?? {}, action.payload);
35
36
  } else {
36
37
  state.manifest = action.payload;
@@ -0,0 +1,11 @@
1
+ import { filter, type OperatorFunction } from 'rxjs';
2
+
3
+ /**
4
+ * RxJS operator that filters out `null` and `undefined` emissions.
5
+ *
6
+ * @template T - The non-nullable value type.
7
+ * @returns An operator that only passes through non-nullable values.
8
+ */
9
+ export function filterEmpty<T>(): OperatorFunction<T | null | undefined, T> {
10
+ return filter((value): value is T => value !== undefined && value !== null);
11
+ }
@@ -0,0 +1,48 @@
1
+ import { from, of, concat } from 'rxjs';
2
+ import { catchError, filter, last, map, share, switchMap } from 'rxjs/operators';
3
+
4
+ import { actions } from '../actions';
5
+
6
+ import type { Flow } from '@equinor/fusion-observable';
7
+ import type { AppModuleProvider } from '../../AppModuleProvider';
8
+ import type { Actions } from '../actions';
9
+ import type { AppBundleState } from '../types';
10
+
11
+ /**
12
+ * Handles the fetch config action by fetching the app configuration from the provider,
13
+ * filtering out null values, and dispatching success or failure actions accordingly.
14
+ *
15
+ * @param provider The AppModuleProvider used to fetch the app configuration.
16
+ * @returns A Flow function that takes an Observable of actions and returns an Observable of actions.
17
+ */
18
+ export const handleFetchConfig =
19
+ (provider: AppModuleProvider): Flow<Actions, AppBundleState> =>
20
+ (action$) =>
21
+ // only handle fetch config request actions
22
+ action$.pipe(
23
+ filter(actions.fetchConfig.match),
24
+ // when request is received, abort any ongoing request and start new
25
+ switchMap(({ payload }) => {
26
+ // TODO(#5127) - use the configUrl directly from the manifest
27
+ // fetch config from provider
28
+ const subject = from(provider.getAppConfig(payload.appKey, payload.build?.version)).pipe(
29
+ // filter out null values
30
+ filter((x) => !!x),
31
+ // allow multiple subscriptions
32
+ share(),
33
+ );
34
+ // first load config and then dispatch success action
35
+ return concat(
36
+ subject.pipe(map((config) => actions.setConfig(config))),
37
+ subject.pipe(
38
+ last(),
39
+ map((config) => actions.fetchConfig.success(config)),
40
+ ),
41
+ ).pipe(
42
+ // catch any error and dispatch failure action
43
+ catchError((err) => {
44
+ return of(actions.fetchConfig.failure(err));
45
+ }),
46
+ );
47
+ }),
48
+ );
@@ -0,0 +1,53 @@
1
+ import { from, of, concat } from 'rxjs';
2
+ import { catchError, filter, last, map, share, switchMap } from 'rxjs/operators';
3
+
4
+ import { actions } from '../actions';
5
+
6
+ import type { Flow } from '@equinor/fusion-observable';
7
+ import type { AppModuleProvider } from '../../AppModuleProvider';
8
+ import type { Actions } from '../actions';
9
+ import type { AppBundleState } from '../types';
10
+
11
+ /**
12
+ * Handles the fetch manifest action by fetching the app manifest from the provider,
13
+ * dispatching success or failure actions based on the result.
14
+ *
15
+ * @param provider The AppModuleProvider used to fetch the app manifest.
16
+ * @returns A Flow function that takes an Observable of actions and returns an Observable of actions.
17
+ */
18
+ export const handleFetchManifest =
19
+ (provider: AppModuleProvider): Flow<Actions, AppBundleState> =>
20
+ (action$) =>
21
+ // only handle fetch manifest request actions
22
+ action$.pipe(
23
+ filter(actions.fetchManifest.match),
24
+ // when request is received, abort any ongoing request and start new
25
+ switchMap((action) => {
26
+ const {
27
+ payload: { key, tag },
28
+ meta: { update },
29
+ } = action;
30
+
31
+ // fetch manifest from provider
32
+ const subject = from(provider.getAppManifest(key, tag)).pipe(
33
+ // filter out null values
34
+ filter((x) => !!x),
35
+ // allow multiple subscriptions
36
+ share(),
37
+ );
38
+
39
+ // first load manifest and then dispatch success action
40
+ return concat(
41
+ subject.pipe(map((manifest) => actions.setManifest(manifest, update))),
42
+ subject.pipe(
43
+ last(),
44
+ map((manifest) => actions.fetchManifest.success(manifest)),
45
+ ),
46
+ ).pipe(
47
+ // catch any error and dispatch failure action
48
+ catchError((err) => {
49
+ return of(actions.fetchManifest.failure(err));
50
+ }),
51
+ );
52
+ }),
53
+ );
@@ -0,0 +1,50 @@
1
+ import { from, of, concat } from 'rxjs';
2
+ import { catchError, filter, last, map, share, switchMap } from 'rxjs/operators';
3
+
4
+ import { actions } from '../actions';
5
+
6
+ import type { Flow } from '@equinor/fusion-observable';
7
+ import type { AppModuleProvider } from '../../AppModuleProvider';
8
+ import type { Actions } from '../actions';
9
+ import type { AppBundleState } from '../types';
10
+
11
+ /**
12
+ * Handles the fetch settings action by fetching the app settings from the provider,
13
+ * filtering out null values, and dispatching success or failure actions accordingly.
14
+ *
15
+ * @param provider The AppModuleProvider used to fetch the app settings.
16
+ * @returns A Flow function that takes an Observable of actions and returns an Observable of actions.
17
+ */
18
+ export const handleFetchSettings =
19
+ (provider: AppModuleProvider): Flow<Actions, AppBundleState> =>
20
+ (action$) =>
21
+ // only handle fetch settings request actions
22
+ action$.pipe(
23
+ filter(actions.fetchSettings.match),
24
+ // when request is received, abort any ongoing request and start new
25
+ switchMap(({ payload }) => {
26
+ const { appKey } = payload;
27
+
28
+ // fetch settings from provider
29
+ const subject = from(provider.getAppSettings(appKey)).pipe(
30
+ // filter out null values
31
+ filter((x) => !!x),
32
+ // allow multiple subscriptions
33
+ share(),
34
+ );
35
+
36
+ // first load settings and then dispatch success action
37
+ return concat(
38
+ subject.pipe(map((settings) => actions.setSettings(settings))),
39
+ subject.pipe(
40
+ last(),
41
+ map((settings) => actions.fetchSettings.success(settings)),
42
+ ),
43
+ ).pipe(
44
+ // catch any error and dispatch failure action
45
+ catchError((err) => {
46
+ return of(actions.fetchSettings.failure(err));
47
+ }),
48
+ );
49
+ }),
50
+ );
@@ -0,0 +1,37 @@
1
+ import { from, of } from 'rxjs';
2
+ import { catchError, filter, map, switchMap } from 'rxjs/operators';
3
+
4
+ import { actions } from '../actions';
5
+
6
+ import type { Flow } from '@equinor/fusion-observable';
7
+ import type { AppModuleProvider } from '../../AppModuleProvider';
8
+ import type { Actions } from '../actions';
9
+ import type { AppBundleState } from '../types';
10
+
11
+ /**
12
+ * Handles the import application flow.
13
+ * @param provider The AppModuleProvider used to resolve the application asset URI.
14
+ * @returns A flow that takes in actions and returns an observable of AppBundleState.
15
+ */
16
+ export const handleImportApplication =
17
+ (provider: AppModuleProvider): Flow<Actions, AppBundleState> =>
18
+ (action$) =>
19
+ // only handle import script request actions
20
+ action$.pipe(
21
+ filter(actions.importApp.match),
22
+ // when request is received, abort any ongoing request and start new
23
+ switchMap(({ payload }) => {
24
+ // dynamically import the application script
25
+ return from(
26
+ import(
27
+ /* @vite-ignore */ /* webpackIgnore: true */
28
+ [provider.assetUri, payload].join('/').replace(/\/{2,}/g, '/')
29
+ ),
30
+ ).pipe(
31
+ // dispatch success action
32
+ map(actions.importApp.success),
33
+ // catch any error and dispatch failure action
34
+ catchError((err) => of(actions.importApp.failure(err))),
35
+ );
36
+ }),
37
+ );
@@ -0,0 +1,39 @@
1
+ import { from, of } from 'rxjs';
2
+ import { catchError, concatMap, filter, last, switchMap } from 'rxjs/operators';
3
+
4
+ import { actions } from '../actions';
5
+
6
+ import type { Flow } from '@equinor/fusion-observable';
7
+ import type { AppModuleProvider } from '../../AppModuleProvider';
8
+ import type { Actions } from '../actions';
9
+ import type { AppBundleState } from '../types';
10
+
11
+ /**
12
+ * Handles the set settings action by setting the app settings from the provider,
13
+ * filtering out null values, and dispatching success or failure actions accordingly.
14
+ *
15
+ * @param provider The AppModuleProvider used to fetch the app settings.
16
+ * @returns A Flow function that takes an Observable of actions and returns an Observable of actions.
17
+ */
18
+ export const handleUpdateSettings =
19
+ (provider: AppModuleProvider): Flow<Actions, AppBundleState> =>
20
+ (action$) => {
21
+ // only handle update settings request actions
22
+ return action$.pipe(
23
+ filter(actions.updateSettings.match),
24
+ switchMap(({ payload }) => {
25
+ const { appKey, settings } = payload;
26
+ // take the last value, then request updating of settings and dispatch success or failure
27
+ return provider.updateAppSettings(appKey, settings).pipe(
28
+ last(),
29
+ concatMap((updatedSettings) =>
30
+ from([
31
+ actions.setSettings(updatedSettings),
32
+ actions.updateSettings.success(updatedSettings),
33
+ ]),
34
+ ),
35
+ catchError((err) => of(actions.updateSettings.failure(err))),
36
+ );
37
+ }),
38
+ );
39
+ };
@@ -0,0 +1,5 @@
1
+ export { handleFetchManifest } from './handle-fetch-manifest';
2
+ export { handleFetchConfig } from './handle-fetch-config';
3
+ export { handleFetchSettings } from './handle-fetch-settings';
4
+ export { handleUpdateSettings } from './handle-update-settings';
5
+ export { handleImportApplication } from './handle-import-application';
package/src/app/types.ts CHANGED
@@ -25,7 +25,7 @@ import type { Actions } from './actions';
25
25
  */
26
26
  export type AppBundleState<
27
27
  TConfig extends ConfigEnvironment = ConfigEnvironment,
28
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ // biome-ignore lint/suspicious/noExplicitAny: default must be bivariant `any`, not `unknown` — `unknown` breaks assignability when a concrete `AppBundleState<TConfig, TModules>` is used where the default-typed generic is expected
29
29
  TModules = any,
30
30
  > = {
31
31
  appKey: string;
@@ -46,6 +46,6 @@ export type AppBundleState<
46
46
  */
47
47
  export type AppBundleStateInitial<
48
48
  TConfig extends ConfigEnvironment = ConfigEnvironment,
49
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ // biome-ignore lint/suspicious/noExplicitAny: see AppBundleState above — default must be bivariant `any`
50
50
  TModules = any,
51
51
  > = Omit<AppBundleState<TConfig, TModules>, 'status'>;