@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.
- package/CHANGELOG.md +19 -0
- package/dist/esm/AppClient.js +75 -8
- package/dist/esm/AppClient.js.map +1 -1
- package/dist/esm/AppConfig.js +9 -1
- package/dist/esm/AppConfig.js.map +1 -1
- package/dist/esm/{AppClient.Selectors.js → AppConfigSelector.js} +1 -1
- package/dist/esm/AppConfigSelector.js.map +1 -0
- package/dist/esm/AppConfigurator.js +28 -2
- package/dist/esm/AppConfigurator.js.map +1 -1
- package/dist/esm/AppModuleProvider.js +16 -0
- package/dist/esm/AppModuleProvider.js.map +1 -1
- package/dist/esm/app/App.js +82 -24
- package/dist/esm/app/App.js.map +1 -1
- package/dist/esm/app/actions.js +2 -2
- package/dist/esm/app/actions.js.map +1 -1
- package/dist/esm/app/create-reducer.js +2 -1
- package/dist/esm/app/create-reducer.js.map +1 -1
- package/dist/esm/app/filter-empty.js +11 -0
- package/dist/esm/app/filter-empty.js.map +1 -0
- package/dist/esm/app/flows/handle-fetch-config.js +30 -0
- package/dist/esm/app/flows/handle-fetch-config.js.map +1 -0
- package/dist/esm/app/flows/handle-fetch-manifest.js +30 -0
- package/dist/esm/app/flows/handle-fetch-manifest.js.map +1 -0
- package/dist/esm/app/flows/handle-fetch-settings.js +30 -0
- package/dist/esm/app/flows/handle-fetch-settings.js.map +1 -0
- package/dist/esm/app/flows/handle-import-application.js +23 -0
- package/dist/esm/app/flows/handle-import-application.js.map +1 -0
- package/dist/esm/app/flows/handle-update-settings.js +22 -0
- package/dist/esm/app/flows/handle-update-settings.js.map +1 -0
- package/dist/esm/app/flows/index.js +6 -0
- package/dist/esm/app/flows/index.js.map +1 -0
- package/dist/esm/enable-app-module.js +2 -1
- package/dist/esm/enable-app-module.js.map +1 -1
- package/dist/esm/errors/AppBuildError.js +35 -0
- package/dist/esm/errors/AppBuildError.js.map +1 -0
- package/dist/esm/errors/AppConfigError.js +35 -0
- package/dist/esm/errors/AppConfigError.js.map +1 -0
- package/dist/esm/errors/AppManifestError.js +35 -0
- package/dist/esm/errors/AppManifestError.js.map +1 -0
- package/dist/esm/errors/AppScriptModuleError.js +17 -0
- package/dist/esm/errors/AppScriptModuleError.js.map +1 -0
- package/dist/esm/errors/AppSettingsError.js +35 -0
- package/dist/esm/errors/AppSettingsError.js.map +1 -0
- package/dist/esm/errors/app-error-type.js +2 -0
- package/dist/esm/errors/app-error-type.js.map +1 -0
- package/dist/esm/errors.js +5 -148
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/module.js +2 -0
- package/dist/esm/module.js.map +1 -1
- package/dist/esm/schemas.js +4 -0
- package/dist/esm/schemas.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/AppClient.d.ts +42 -0
- package/dist/types/AppConfig.d.ts +7 -1
- package/dist/types/AppConfigurator.d.ts +25 -0
- package/dist/types/AppModuleProvider.d.ts +10 -0
- package/dist/types/app/App.d.ts +49 -8
- package/dist/types/app/create-reducer.d.ts +55 -55
- package/dist/types/app/filter-empty.d.ts +8 -0
- package/dist/types/app/flows/handle-fetch-config.d.ts +12 -0
- package/dist/types/app/flows/handle-fetch-manifest.d.ts +12 -0
- package/dist/types/app/flows/handle-fetch-settings.d.ts +12 -0
- package/dist/types/app/flows/handle-import-application.d.ts +10 -0
- package/dist/types/app/flows/handle-update-settings.d.ts +12 -0
- package/dist/types/app/flows/index.d.ts +5 -0
- package/dist/types/errors/AppBuildError.d.ts +21 -0
- package/dist/types/errors/AppConfigError.d.ts +21 -0
- package/dist/types/errors/AppManifestError.d.ts +21 -0
- package/dist/types/errors/AppScriptModuleError.d.ts +14 -0
- package/dist/types/errors/AppSettingsError.d.ts +21 -0
- package/dist/types/errors/app-error-type.d.ts +9 -0
- package/dist/types/errors.d.ts +6 -103
- package/dist/types/version.d.ts +1 -1
- package/package.json +10 -10
- package/src/AppClient.ts +65 -4
- package/src/AppConfig.ts +9 -1
- package/src/AppConfigurator.ts +28 -4
- package/src/AppModuleProvider.ts +16 -0
- package/src/app/App.ts +88 -31
- package/src/app/actions.ts +2 -2
- package/src/app/create-reducer.ts +2 -1
- package/src/app/filter-empty.ts +11 -0
- package/src/app/flows/handle-fetch-config.ts +48 -0
- package/src/app/flows/handle-fetch-manifest.ts +53 -0
- package/src/app/flows/handle-fetch-settings.ts +50 -0
- package/src/app/flows/handle-import-application.ts +37 -0
- package/src/app/flows/handle-update-settings.ts +39 -0
- package/src/app/flows/index.ts +5 -0
- package/src/app/types.ts +2 -2
- package/src/enable-app-module.ts +2 -1
- package/src/errors/AppBuildError.ts +47 -0
- package/src/errors/AppConfigError.ts +47 -0
- package/src/errors/AppManifestError.ts +47 -0
- package/src/errors/AppScriptModuleError.ts +20 -0
- package/src/errors/AppSettingsError.ts +47 -0
- package/src/errors/app-error-type.ts +9 -0
- package/src/errors.ts +6 -208
- package/src/module.ts +2 -0
- package/src/schemas.ts +4 -0
- package/src/types.ts +1 -1
- package/src/version.ts +1 -1
- package/dist/esm/AppClient.Selectors.js.map +0 -1
- package/dist/esm/app/flows.js +0 -120
- package/dist/esm/app/flows.js.map +0 -1
- package/dist/types/app/flows.d.ts +0 -41
- package/src/app/flows.ts +0 -189
- /package/dist/types/{AppClient.Selectors.d.ts → AppConfigSelector.d.ts} +0 -0
- /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
|
-
|
|
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
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
//
|
|
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()
|
|
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;
|
package/src/app/actions.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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'>;
|