@equinor/fusion-framework-module-bookmark 2.2.0 → 3.0.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.
@@ -1,4 +1,4 @@
1
- import { Observable, Subscription, forkJoin, from, lastValueFrom, of } from 'rxjs';
1
+ import { Observable, Subscription, forkJoin, from, lastValueFrom, of, defer } from 'rxjs';
2
2
 
3
3
  import {
4
4
  filter,
@@ -12,7 +12,7 @@ import {
12
12
  catchError,
13
13
  } from 'rxjs/operators';
14
14
 
15
- import { produce, castDraft } from 'immer';
15
+ import { castDraft, createDraft, finishDraft } from 'immer';
16
16
 
17
17
  import { v4 as generateGUID } from 'uuid';
18
18
 
@@ -61,7 +61,8 @@ import type {
61
61
  IBookmarkProvider,
62
62
  } from './BookmarkProvider.interface';
63
63
 
64
- const defaultTimeout = 2 * 60 * 1000; // 2 minutes
64
+ // Default timeout for bookmark operations (2 minutes)
65
+ const defaultTimeout = 2 * 60 * 1000;
65
66
 
66
67
  /**
67
68
  * The `BookmarkProvider` class is responsible for managing bookmarks in the application.
@@ -356,9 +357,11 @@ export class BookmarkProvider implements IBookmarkProvider {
356
357
  ): VoidFunction {
357
358
  if (!this._event) {
358
359
  this._log?.warn('Failed to register event listener, event provider not configured');
359
- return () => {};
360
+ return () => {
361
+ // No-op function when event provider is not configured
362
+ };
360
363
  }
361
- return this._event?.addEventListener(eventName, (event) => {
364
+ return this._event.addEventListener(eventName, (event) => {
362
365
  if (event.source === this) {
363
366
  callback(event);
364
367
  }
@@ -398,17 +401,21 @@ export class BookmarkProvider implements IBookmarkProvider {
398
401
  * @returns An observable that emits the generated payload.
399
402
  * @template T - The type of the generated payload.
400
403
  */
401
- public generatePayload<T extends BookmarkData>(
404
+ public generatePayload<T extends BookmarkData = BookmarkData>(
402
405
  initial?: Partial<T> | null,
403
406
  ): Observable<T | null | undefined> {
404
- this._log?.debug(`generating payload`, initial);
407
+ this._log?.debug('generating payload', initial);
405
408
 
406
409
  /**
407
410
  * Observable that emits the generated payload.
408
411
  */
409
412
  const result$ = from(this.#payloadGenerators).pipe(
410
- mergeScan((acc, generator) => this._producePayload(acc, generator), initial ?? {}, 1),
411
- tap((payload) => this._log?.debug(`generated payload`, { initial, payload })),
413
+ mergeScan(
414
+ (acc, generator) => from(Promise.resolve(this._producePayload(acc, generator))),
415
+ initial ?? ({} as Partial<T>),
416
+ 1,
417
+ ),
418
+ tap((payload) => this._log?.debug('generated payload', { initial, payload })),
412
419
  ) as Observable<T | null | undefined>;
413
420
 
414
421
  return result$;
@@ -428,35 +435,41 @@ export class BookmarkProvider implements IBookmarkProvider {
428
435
  * If the generator returns a value, a warning will be logged.
429
436
  * If the generator returns null, an info message will be logged indicating that the bookmark data should be cleared.
430
437
  */
431
- protected _producePayload<T extends BookmarkData>(
438
+ protected async _producePayload<T extends BookmarkData = BookmarkData>(
432
439
  value: Partial<T>,
433
440
  generator: BookmarkPayloadGenerator<T>,
434
441
  initial?: Partial<T> | null,
435
- ) {
436
- // produce payload from generator
437
- return produce(value, async (draft) => {
438
- try {
439
- const result = await Promise.resolve(generator(draft as Partial<T>, initial));
440
- // cast the result to a draft object
441
- if (result) {
442
- this._log?.warn(
443
- `bookmark data generator ${generator.name} returned a value, but it should not do this, since the data is an immer draft object`,
444
- );
445
- // Dirty fix since some developers are returning the reference object, which will freeze the object
446
- return castDraft(JSON.parse(JSON.stringify(result)));
447
- }
442
+ ): Promise<T | null | undefined> {
443
+ // Create a draft for async operations using createDraft/finishDraft pattern
444
+ const draft = createDraft(value);
448
445
 
449
- // clear the bookmark data if the generator returns null
450
- if (result === null) {
451
- this._log?.info(
452
- `bookmark data generator ${generator.name} wish to clear the bookmark data`,
453
- );
454
- return undefined;
455
- }
456
- } catch (error) {
457
- this._log?.error(`Failed to produce payload using generator ${generator.name}`, error);
446
+ try {
447
+ const generatorResult = await Promise.resolve(generator(draft as Partial<T>, initial));
448
+
449
+ // Handle the generator result
450
+ if (generatorResult) {
451
+ this._log?.warn(
452
+ `bookmark data generator ${generator.name} returned a value, but it should not do this, since the data is an immer draft object`,
453
+ );
454
+ // Dirty fix since some developers are returning the reference object, which will freeze the object
455
+ return castDraft(generatorResult) as T | null | undefined;
458
456
  }
459
- });
457
+
458
+ // clear the bookmark data if the generator returns null
459
+ if (generatorResult === null) {
460
+ this._log?.info(
461
+ `bookmark data generator ${generator.name} wish to clear the bookmark data`,
462
+ );
463
+ return undefined;
464
+ }
465
+ } catch (error) {
466
+ this._log?.error(`Failed to produce payload using generator ${generator.name}`, error);
467
+ }
468
+
469
+ // Finish the draft to get the immutable result
470
+ const result = finishDraft(draft);
471
+
472
+ return result as T | null | undefined;
460
473
  }
461
474
 
462
475
  /**
@@ -466,10 +479,10 @@ export class BookmarkProvider implements IBookmarkProvider {
466
479
  * @param options An optional object that allows excluding the bookmark payload from the result.
467
480
  * @returns An observable that emits the combined result of fetching the bookmark and bookmark data.
468
481
  */
469
- public getBookmark<T extends BookmarkData>(
482
+ public getBookmark<T extends BookmarkData = BookmarkData>(
470
483
  bookmarkId: string,
471
484
  options?: { excludePayload?: boolean },
472
- ) {
485
+ ): Observable<Bookmark<T>> {
473
486
  this._log?.debug(`fetching bookmark: ${bookmarkId}`, options);
474
487
 
475
488
  // fetch the bookmark and bookmark data
@@ -497,7 +510,7 @@ export class BookmarkProvider implements IBookmarkProvider {
497
510
  * @param options.excludePayload - Specifies whether to exclude the payload from the bookmark.
498
511
  * @returns A promise that resolves to the retrieved bookmark, or null if the bookmark is not found.
499
512
  */
500
- public getBookmarkAsync<T extends BookmarkData>(
513
+ public getBookmarkAsync<T extends BookmarkData = BookmarkData>(
501
514
  id: string,
502
515
  options?: { excludePayload?: boolean },
503
516
  ): Promise<Bookmark<T> | null> {
@@ -505,24 +518,59 @@ export class BookmarkProvider implements IBookmarkProvider {
505
518
  }
506
519
 
507
520
  /**
508
- * Asynchronously retrieves all bookmarks from the store.
521
+ * Retrieves all bookmarks from the store as an Observable stream.
509
522
  *
510
- * @returns A Promise that resolves to the Bookmarks object containing all bookmarks.
523
+ * This method implements a complex flow that:
524
+ * 1. Resolves filter parameters (sourceSystem, appKey, contextId) based on configuration
525
+ * 2. Dispatches a fetch action to the store with resolved filters
526
+ * 3. Monitors store actions for success/failure responses
527
+ * 4. Returns the bookmarks data or throws appropriate errors
528
+ *
529
+ * @remarks
530
+ * The method uses a reactive pattern where filter resolution and store actions are
531
+ * handled asynchronously. If filtering is enabled, it will resolve the current
532
+ * application and context to filter bookmarks accordingly.
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * // Basic usage
537
+ * bookmarkProvider.getAllBookmarks().subscribe({
538
+ * next: (bookmarks) => console.log('Retrieved bookmarks:', bookmarks),
539
+ * error: (err) => console.error('Failed to fetch bookmarks:', err)
540
+ * });
541
+ *
542
+ * // With filtering enabled in config
543
+ * const config = {
544
+ * filters: { context: true, application: true }
545
+ * };
546
+ * // Will automatically filter by current context and application
547
+ * ```
548
+ *
549
+ * @returns An Observable that emits the array of bookmarks when successfully retrieved
550
+ * @throws {BookmarkProviderError} When filter resolution fails or store operations fail
551
+ * @throws {BookmarkProviderError} When a timeout occurs during the fetch operation
511
552
  */
512
553
  public getAllBookmarks(): Observable<Bookmarks> {
513
554
  return new Observable<Bookmarks>((observer) => {
514
- this._log?.debug(`fetching all bookmarks`);
555
+ this._log?.debug('fetching all bookmarks');
515
556
 
516
- // generate the filter parameters
557
+ // Step 1: Generate filter parameters based on configuration
558
+ // This creates a stream that resolves all necessary filter values
517
559
  const filter$ = forkJoin({
560
+ // Always include the source system for the current provider
518
561
  sourceSystem: of(this.sourceSystem),
562
+
563
+ // Resolve application key if filtering by application is enabled
519
564
  appKey: this.#config.filters?.application
520
- ? from(this._resolve.application()).pipe(map((x) => x?.appKey))
565
+ ? defer(() => this._resolve.application()).pipe(map((x) => x?.appKey))
521
566
  : of(undefined),
567
+
568
+ // Resolve context ID if filtering by context is enabled
522
569
  contextId: this.#config.filters?.context
523
- ? from(this._resolve.context()).pipe(map((x) => x?.id))
570
+ ? defer(() => this._resolve.context()).pipe(map((x) => x?.id))
524
571
  : of(undefined),
525
572
  }).pipe(
573
+ // Handle any errors during filter parameter resolution
526
574
  catchError((err) => {
527
575
  const error = new BookmarkProviderError(
528
576
  'Could not fetch bookmarks, failed to resolve filter parameters',
@@ -533,20 +581,25 @@ export class BookmarkProvider implements IBookmarkProvider {
533
581
  }),
534
582
  );
535
583
 
536
- // execute the fetch bookmarks action when the filter parameters are resolved
584
+ // Step 2: Dispatch fetch action to store when filter parameters are ready
585
+ // This triggers the actual API call through the store's action system
586
+ // The store will handle the async operation and emit success/failure actions
587
+ // Using observer.add() ensures proper cleanup when the Observable is unsubscribed
537
588
  observer.add(
538
589
  filter$.subscribe((filter) => {
539
590
  this.#store.next(bookmarkActions.fetchBookmarks(filter));
540
591
  }),
541
592
  );
542
593
 
543
- // monitor the failure case when fetching bookmarks
594
+ // Step 3: Monitor for failure responses from the store
595
+ // This stream will emit and throw an error if the fetch operation fails
544
596
  const failure$ = this.#store.action$.pipe(
545
597
  filter(bookmarkActions.fetchBookmarks.failure.match),
546
598
  map((action) => {
547
599
  this._log?.error('Failed to fetch bookmarks', action.payload);
548
600
  throw new BookmarkProviderError('Failed to fetch bookmarks', action.payload);
549
601
  }),
602
+ // Add timeout protection to prevent hanging requests
550
603
  timeout({
551
604
  each: defaultTimeout,
552
605
  with: () => {
@@ -555,17 +608,22 @@ export class BookmarkProvider implements IBookmarkProvider {
555
608
  }),
556
609
  );
557
610
 
558
- // monitor the success case when fetching bookmarks
611
+ // Step 4: Monitor for success responses from the store
612
+ // This stream will emit the bookmarks data when successfully retrieved
559
613
  const request$ = this.#store.action$.pipe(
560
614
  filter(bookmarkActions.fetchBookmarks.success.match),
561
615
  map((a) => a.payload),
562
616
  tap((bookmarks) => {
563
- this._log?.debug(`fetched all bookmarks`, bookmarks);
617
+ this._log?.debug('fetched all bookmarks', bookmarks);
564
618
  }),
619
+ // Race against failure stream - whichever completes first wins
620
+ // This ensures we either get the bookmarks data or an error, not both
565
621
  raceWith(failure$),
622
+ // Only take the first emission (success or failure) to complete the stream
566
623
  first(),
567
624
  );
568
625
 
626
+ // Step 5: Subscribe to the request stream and forward results to observer
569
627
  request$.subscribe(observer);
570
628
  });
571
629
  }
@@ -590,7 +648,7 @@ export class BookmarkProvider implements IBookmarkProvider {
590
648
  * @returns An observable that emits the next bookmark or null.
591
649
  * @template T - The type of the bookmark data.
592
650
  */
593
- public setCurrentBookmark<T extends BookmarkData>(
651
+ public setCurrentBookmark<T extends BookmarkData = BookmarkData>(
594
652
  bookmark_or_id: Bookmark<T> | string | null,
595
653
  ): Observable<Bookmark<T> | null> {
596
654
  this._log?.debug('setting current bookmark', bookmark_or_id);
@@ -642,7 +700,7 @@ export class BookmarkProvider implements IBookmarkProvider {
642
700
  * @param bookmarkId - The ID of the bookmark to set as the current bookmark.
643
701
  * @returns A subscription to the operation that sets the current bookmark.
644
702
  */
645
- public setCurrentBookmarkAsync<T extends BookmarkData>(
703
+ public setCurrentBookmarkAsync<T extends BookmarkData = BookmarkData>(
646
704
  bookmark_or_id: Bookmark<T> | string | null,
647
705
  ): Promise<Bookmark<T> | null> {
648
706
  return lastValueFrom(this.setCurrentBookmark<T>(bookmark_or_id));
@@ -650,116 +708,121 @@ export class BookmarkProvider implements IBookmarkProvider {
650
708
 
651
709
  /**
652
710
  * Creates a new bookmark with the provided bookmark data.
711
+ * The creation executes immediately when this method is called.
653
712
  * @template T - The type of bookmark data.
654
713
  * @param {BookmarkCreateArgs<T>} newBookmarkData - The data for creating the bookmark.
655
714
  * @returns {Observable<Bookmark<T>>} - An observable that emits the created bookmark.
656
715
  */
657
- public createBookmark<T extends BookmarkData>(
716
+ public createBookmark<T extends BookmarkData = BookmarkData>(
658
717
  newBookmarkData: BookmarkCreateArgs<T>,
659
718
  ): Observable<Bookmark<T>> {
660
- return new Observable<Bookmark<T>>((subscriber) => {
661
- const { ref, action$ } = this._useScopedActions();
719
+ const { ref, action$ } = this._useScopedActions();
720
+
721
+ this._log?.debug(`creating new bookmark, ref: ${ref}`, newBookmarkData);
722
+
723
+ // resolve the bookmark
724
+ const bookmark$ = forkJoin({
725
+ appKey: newBookmarkData.appKey
726
+ ? of(newBookmarkData.appKey)
727
+ : defer(() => this._resolve.application()).pipe(
728
+ map((app) => {
729
+ if (!app?.appKey) {
730
+ throw new BookmarkProviderError('Failed to resolve application key');
731
+ }
732
+ return app.appKey;
733
+ }),
734
+ ),
735
+ contextId: defer(() => this._resolve.context()).pipe(map((context) => context?.id)),
736
+ payload: this.generatePayload<T>(newBookmarkData.payload),
737
+ sourceSystem: of(this.sourceSystem),
738
+ }).pipe(
739
+ catchError((err) => {
740
+ const error = new BookmarkProviderError(
741
+ 'Could not create new bookmark, failed to resolve bookmark data',
742
+ err,
743
+ );
744
+ this._log?.error(error.message, error);
745
+ throw error;
746
+ }),
747
+ // merge the resolved data with the new bookmark data
748
+ map(
749
+ (resolvedData) =>
750
+ ({
751
+ ...newBookmarkData,
752
+ ...resolvedData,
753
+ }) as BookmarkNew<T>,
754
+ ),
755
+ );
662
756
 
663
- this._log?.debug(`creating new bookmark, ref: ${ref}`, newBookmarkData);
664
-
665
- // resolve the bookmark
666
- const bookmark$ = forkJoin({
667
- appKey: newBookmarkData.appKey
668
- ? of(newBookmarkData.appKey)
669
- : from(this._resolve.application()).pipe(
670
- map((app) => {
671
- if (!app?.appKey) {
672
- throw new BookmarkProviderError('Failed to resolve application key');
673
- }
674
- return app.appKey;
675
- }),
676
- ),
677
- contextId: this._resolve.context().then((context) => context?.id),
678
- payload: this.generatePayload<T>(newBookmarkData.payload),
679
- sourceSystem: of(this.sourceSystem),
680
- }).pipe(
681
- catchError((err) => {
757
+ // notify listeners that a bookmark is about to be created
758
+ const dispatch$ = bookmark$.pipe(
759
+ switchMap(async (bookmark) => {
760
+ // notify listeners that a bookmark is about to be created
761
+ const { canceled, type } = await this._dispatchEvent('onBookmarkCreate', {
762
+ detail: bookmark,
763
+ cancelable: true,
764
+ });
765
+
766
+ // throw an error if the event is canceled
767
+ if (canceled) {
682
768
  const error = new BookmarkProviderError(
683
- 'Could not create new bookmark, failed to resolve bookmark data',
684
- err,
769
+ `event: ${type} was canceled by listener for creating bookmark: ${ref}`,
685
770
  );
686
- this._log?.error(error.message, error);
771
+ this._log?.info(error.message);
687
772
  throw error;
688
- }),
689
- // merge the resolved data with the new bookmark data
690
- map(
691
- (resolvedData) =>
692
- ({
693
- ...newBookmarkData,
694
- ...resolvedData,
695
- }) as BookmarkNew<T>,
696
- ),
697
- );
698
-
699
- // notify listeners that a bookmark is about to be created
700
- const dispatch$ = bookmark$.pipe(
701
- switchMap(async (bookmark) => {
702
- // notify listeners that a bookmark is about to be created
703
- const { canceled, type } = await this._dispatchEvent('onBookmarkCreate', {
704
- detail: bookmark,
705
- cancelable: true,
706
- });
773
+ }
707
774
 
708
- // throw an error if the event is canceled
709
- if (canceled) {
710
- const error = new BookmarkProviderError(
711
- `event: ${type} was canceled by listener for creating bookmark: ${ref}`,
712
- );
713
- this._log?.info(error.message);
714
- throw error;
715
- }
775
+ return bookmark;
776
+ }),
777
+ );
716
778
 
717
- return bookmark;
718
- }),
719
- );
779
+ // Start the creation process immediately
780
+ const createSubscription = dispatch$.subscribe({
781
+ error: (error) => {
782
+ this._log?.error(`Failed to prepare bookmark creation: ${ref}`, error);
783
+ },
784
+ next: (newBookmark) => {
785
+ // request the store to create the bookmark
786
+ this.#store.next(bookmarkActions.createBookmark(newBookmark, { ref }));
787
+ },
788
+ });
720
789
 
721
- // execute the create bookmark action when the bookmark is resolved and not canceled
722
- subscriber.add(
723
- dispatch$.subscribe({
724
- error: (error) => subscriber.error(error),
725
- next: (newBookmark) => {
726
- // request the store to create the bookmark
727
- this.#store.next(bookmarkActions.createBookmark(newBookmark, { ref }));
728
- },
729
- }),
730
- );
790
+ // monitor the failure case when creating a bookmark
791
+ const failure$ = action$.pipe(
792
+ filter(bookmarkActions.createBookmark.failure.match),
793
+ map(({ payload: cause }) => {
794
+ const error = new BookmarkProviderError(`Failed to create bookmark: ${ref}`, {
795
+ cause,
796
+ });
797
+ this._log?.warn(error.message);
798
+ throw error;
799
+ }),
800
+ timeout({
801
+ each: defaultTimeout,
802
+ with: () => {
803
+ throw new BookmarkProviderError(`Timeout while creating bookmark: ${ref}`);
804
+ },
805
+ }),
806
+ );
731
807
 
732
- // monitor the failure case when creating a bookmark
733
- const failure$ = action$.pipe(
734
- filter(bookmarkActions.createBookmark.failure.match),
735
- map(({ payload: cause }) => {
736
- const error = new BookmarkProviderError(`Failed to create bookmark: ${ref}`, {
737
- cause,
738
- });
739
- this._log?.warn(error.message);
740
- throw error;
741
- }),
742
- timeout({
743
- each: defaultTimeout,
744
- with: () => {
745
- throw new BookmarkProviderError(`Timeout while creating bookmark: ${ref}`);
746
- },
747
- }),
748
- );
808
+ // monitor the success case when creating a bookmark
809
+ const request$ = action$.pipe(
810
+ filter(bookmarkActions.createBookmark.success.match),
811
+ map(({ payload }) => payload as Bookmark<T>),
812
+ tap((bookmark) => {
813
+ this._log?.info(`Bookmark created: ${bookmark.id}, ref: ${ref}`);
814
+ this._dispatchEvent('onBookmarkCreated', { detail: bookmark });
815
+ }),
816
+ raceWith(failure$),
817
+ first(),
818
+ );
749
819
 
750
- // monitor the success case when creating a bookmark
751
- const request$ = action$.pipe(
752
- filter(bookmarkActions.createBookmark.success.match),
753
- map(({ payload }) => payload as Bookmark<T>),
754
- tap((bookmark) => {
755
- this._log?.info(`Bookmark created: ${bookmark.id}, ref: ${ref}`);
756
- this._dispatchEvent('onBookmarkCreated', { detail: bookmark });
757
- }),
758
- raceWith(failure$),
759
- first(),
760
- );
820
+ // Return the observable that will emit the result
821
+ return new Observable<Bookmark<T>>((subscriber) => {
822
+ // Clean up the create subscription when the result observable is unsubscribed
823
+ subscriber.add(() => createSubscription.unsubscribe());
761
824
 
762
- // emit the created bookmark
825
+ // Emit the created bookmark
763
826
  request$.subscribe(subscriber);
764
827
  });
765
828
  }
@@ -770,7 +833,7 @@ export class BookmarkProvider implements IBookmarkProvider {
770
833
  * @param bookmark - The new bookmark to create.
771
834
  * @returns A promise that resolves to the created bookmark with its associated data.
772
835
  */
773
- public createBookmarkAsync<T extends BookmarkData>(
836
+ public createBookmarkAsync<T extends BookmarkData = BookmarkData>(
774
837
  args: BookmarkCreateArgs<T>,
775
838
  ): Promise<Bookmark<T>> {
776
839
  return lastValueFrom(this.createBookmark<T>(args));
@@ -778,14 +841,15 @@ export class BookmarkProvider implements IBookmarkProvider {
778
841
 
779
842
  /**
780
843
  * Updates a bookmark with the specified bookmarkId and bookmarkUpdates.
844
+ * The update executes immediately when this method is called.
781
845
  *
782
846
  * @template T - The type of the bookmark data.
783
847
  * @param {string} bookmarkId - The identifier of the bookmark to update.
784
848
  * @param {BookmarkUpdate<T>} [bookmarkUpdates] - The updates to apply to the bookmark.
785
849
  * @param {BookmarkUpdateOptions} [options] - The options for updating the bookmark.
786
- * @returns {Promise<Bookmark<T>>} - A promise that resolves to the updated bookmark.
850
+ * @returns {Observable<Bookmark<T>>} - An observable that emits the updated bookmark.
787
851
  */
788
- public updateBookmark<T extends BookmarkData>(
852
+ public updateBookmark<T extends BookmarkData = BookmarkData>(
789
853
  bookmarkId: string,
790
854
  bookmarkUpdates?: BookmarkUpdate<T>,
791
855
  options?: BookmarkUpdateOptions,
@@ -796,101 +860,105 @@ export class BookmarkProvider implements IBookmarkProvider {
796
860
  );
797
861
  }
798
862
 
799
- return new Observable<Bookmark<T>>((subscriber) => {
800
- const { ref, action$ } = this._useScopedActions();
863
+ const { ref, action$ } = this._useScopedActions();
801
864
 
802
- this._log?.debug(`Updating bookmark: ${bookmarkId}, ref: ${ref}`);
803
-
804
- /**
805
- * Generate updates with payload
806
- * @remarks
807
- * If `excludePayloadGeneration` is `true`, it emits the `bookmarkUpdates` directly.
808
- * If `excludePayloadGeneration` is `false`, it generates the payload using the `generatePayload` method and emits the updated `bookmarkUpdates` with the generated payload.
809
- */
810
- const updates$ = options?.excludePayloadGeneration
811
- ? of(bookmarkUpdates as BookmarkUpdate<T>)
812
- : this.generatePayload<T>(bookmarkUpdates?.payload).pipe(
813
- map(
814
- (payload) =>
815
- ({
816
- ...bookmarkUpdates,
817
- payload,
818
- }) as BookmarkUpdate<T>,
819
- ),
820
- );
865
+ this._log?.debug(`Updating bookmark: ${bookmarkId}, ref: ${ref}`);
821
866
 
822
- // notify listeners that a bookmark is about to be updated
823
- const dispatch$ = updates$.pipe(
824
- switchMap(async (updates) => {
825
- const { canceled, type } = await this._dispatchEvent('onBookmarkUpdate', {
826
- detail: {
827
- current: bookmarkSelector(this.#store.value, bookmarkId),
828
- updates,
829
- },
830
- cancelable: true,
831
- });
832
-
833
- if (canceled) {
834
- const error = new BookmarkProviderError(
835
- `event: ${type} was canceled by listener for updating bookmark: ${bookmarkId}, ref: ${ref}`,
836
- );
837
- this._log?.warn(error.message, updates);
838
- throw error;
839
- }
840
-
841
- return updates;
842
- }),
843
- );
867
+ /**
868
+ * Generate updates with payload
869
+ * @remarks
870
+ * If `excludePayloadGeneration` is `true`, it emits the `bookmarkUpdates` directly.
871
+ * If `excludePayloadGeneration` is `false`, it generates the payload using the `generatePayload` method and emits the updated `bookmarkUpdates` with the generated payload.
872
+ */
873
+ const updates$ = options?.excludePayloadGeneration
874
+ ? of(bookmarkUpdates as BookmarkUpdate<T>)
875
+ : this.generatePayload<T>(bookmarkUpdates?.payload).pipe(
876
+ map(
877
+ (payload) =>
878
+ ({
879
+ ...bookmarkUpdates,
880
+ payload,
881
+ }) as BookmarkUpdate<T>,
882
+ ),
883
+ );
844
884
 
845
- // execute the update bookmark action when the updates are resolved and not canceled
846
- subscriber.add(
847
- dispatch$.subscribe({
848
- error: (error) => subscriber.error(error),
849
- next: (updates) => {
850
- // trigger the store to update the bookmark
851
- this.#store.next(bookmarkActions.updateBookmark({ bookmarkId, updates }, { ref }));
885
+ // notify listeners that a bookmark is about to be updated
886
+ const dispatch$ = updates$.pipe(
887
+ switchMap(async (updates) => {
888
+ const { canceled, type } = await this._dispatchEvent('onBookmarkUpdate', {
889
+ detail: {
890
+ current: bookmarkSelector(this.#store.value, bookmarkId),
891
+ updates,
852
892
  },
853
- }),
854
- );
893
+ cancelable: true,
894
+ });
855
895
 
856
- // monitor the failure case when updating a bookmark
857
- const failure$ = action$.pipe(
858
- filter(bookmarkActions.updateBookmark.failure.match),
859
- map(({ payload: cause }) => {
860
- const error = new BookmarkProviderError(`Failed to update bookmark: ${bookmarkId}`, {
861
- cause,
862
- });
863
- this._log?.info(error.message);
896
+ if (canceled) {
897
+ const error = new BookmarkProviderError(
898
+ `event: ${type} was canceled by listener for updating bookmark: ${bookmarkId}, ref: ${ref}`,
899
+ );
900
+ this._log?.warn(error.message, updates);
864
901
  throw error;
865
- }),
866
- timeout({
867
- each: defaultTimeout,
868
- with: () => {
869
- throw new BookmarkProviderError(`Timeout while updating bookmark: ${bookmarkId}`);
870
- },
871
- }),
872
- );
902
+ }
873
903
 
874
- // monitor the success case when updating a bookmark
875
- const request$ = action$.pipe(
876
- filter(bookmarkActions.updateBookmark.success.match),
877
- map(
878
- // TODO: add payload if current bookmark is the same as the updated bookmark
879
- ({ payload }): Bookmark<T> =>
880
- ({
881
- ...bookmarkSelector(this.#store.value, payload.id),
882
- payload: payload.payload,
883
- }) as Bookmark<T>,
884
- ),
885
- tap((bookmark) => {
886
- this._log?.info(`Bookmark updated: ${bookmark.id}, ref: ${ref}`);
887
- this._dispatchEvent('onBookmarkUpdated', { detail: bookmark });
888
- }),
889
- raceWith(failure$),
890
- first(),
891
- );
904
+ return updates;
905
+ }),
906
+ );
892
907
 
893
- // emit the updated bookmark
908
+ // Start the update process immediately
909
+ const updateSubscription = dispatch$.subscribe({
910
+ error: (error) => {
911
+ this._log?.error(`Failed to prepare bookmark update: ${bookmarkId}`, error);
912
+ },
913
+ next: (updates) => {
914
+ // trigger the store to update the bookmark
915
+ this.#store.next(bookmarkActions.updateBookmark({ bookmarkId, updates }, { ref }));
916
+ },
917
+ });
918
+
919
+ // monitor the failure case when updating a bookmark
920
+ const failure$ = action$.pipe(
921
+ filter(bookmarkActions.updateBookmark.failure.match),
922
+ map(({ payload: cause }) => {
923
+ const error = new BookmarkProviderError(`Failed to update bookmark: ${bookmarkId}`, {
924
+ cause,
925
+ });
926
+ this._log?.info(error.message);
927
+ throw error;
928
+ }),
929
+ timeout({
930
+ each: defaultTimeout,
931
+ with: () => {
932
+ throw new BookmarkProviderError(`Timeout while updating bookmark: ${bookmarkId}`);
933
+ },
934
+ }),
935
+ );
936
+
937
+ // monitor the success case when updating a bookmark
938
+ const request$ = action$.pipe(
939
+ filter(bookmarkActions.updateBookmark.success.match),
940
+ map(
941
+ // TODO: add payload if current bookmark is the same as the updated bookmark
942
+ ({ payload }): Bookmark<T> =>
943
+ ({
944
+ ...bookmarkSelector(this.#store.value, payload.id),
945
+ payload: payload.payload,
946
+ }) as Bookmark<T>,
947
+ ),
948
+ tap((bookmark) => {
949
+ this._log?.info(`Bookmark updated: ${bookmark.id}, ref: ${ref}`);
950
+ this._dispatchEvent('onBookmarkUpdated', { detail: bookmark });
951
+ }),
952
+ raceWith(failure$),
953
+ first(),
954
+ );
955
+
956
+ // Return the observable that will emit the result
957
+ return new Observable<Bookmark<T>>((subscriber) => {
958
+ // Clean up the update subscription when the result observable is unsubscribed
959
+ subscriber.add(() => updateSubscription.unsubscribe());
960
+
961
+ // Emit the updated bookmark
894
962
  request$.subscribe(subscriber);
895
963
  });
896
964
  }
@@ -903,7 +971,7 @@ export class BookmarkProvider implements IBookmarkProvider {
903
971
  * @param bookmark - The bookmark to update.
904
972
  * @returns A promise that resolves to the updated bookmark with its associated data.
905
973
  */
906
- public updateBookmarkAsync<T extends BookmarkData>(
974
+ public updateBookmarkAsync<T extends BookmarkData = BookmarkData>(
907
975
  id_or_bookmark: string | Bookmark<T>,
908
976
  updates_or_options?: BookmarkUpdate<T> | BookmarkUpdateOptions,
909
977
  options?: BookmarkUpdateOptions,
@@ -921,10 +989,7 @@ export class BookmarkProvider implements IBookmarkProvider {
921
989
  updatePayload: boolean;
922
990
  }
923
991
  ).updatePayload,
924
- }).pipe(
925
- // this is totally wrong, but we need to keep the API for now
926
- map((bookmark) => bookmark.payload as Bookmark<T>),
927
- ),
992
+ }),
928
993
  );
929
994
  }
930
995
  return lastValueFrom(
@@ -934,83 +999,88 @@ export class BookmarkProvider implements IBookmarkProvider {
934
999
 
935
1000
  /**
936
1001
  * Deletes a bookmark with the specified bookmarkId.
1002
+ * The deletion executes immediately when this method is called.
937
1003
  *
938
1004
  * @param bookmarkId - The unique identifier of the bookmark to be deleted.
939
- * @returns A Promise that resolves when the bookmark is successfully deleted.
1005
+ * @returns An Observable that emits when the bookmark is successfully deleted.
940
1006
  * @throws {BookmarkProviderError} If there is an error deleting the bookmark.
941
1007
  */
942
1008
  public deleteBookmark(bookmarkId: string): Observable<void> {
943
- return new Observable<void>((subscriber) => {
944
- const { ref, action$ } = this._useScopedActions();
945
-
946
- this._log?.debug(`Removing bookmark: ${bookmarkId}, ref: ${ref}`);
1009
+ const { ref, action$ } = this._useScopedActions();
947
1010
 
948
- // bookmark to delete
949
- const bookmark = bookmarkSelector(this.#store.value, bookmarkId) ?? {
950
- id: bookmarkId,
951
- };
952
-
953
- // observable that dispatches the 'onBookmarkDelete' event and maps the result
954
- const dispatch$ = from(
955
- this._dispatchEvent('onBookmarkDelete', {
956
- detail: bookmark,
957
- cancelable: true,
958
- }),
959
- ).pipe(
960
- map(({ canceled, type, detail }) => {
961
- if (canceled) {
962
- const error = new BookmarkProviderError(
963
- `event: ${type} was canceled by listener for removing bookmark: ${bookmarkId}, ref: ${ref}`,
964
- );
965
- this._log?.warn(error.message);
966
- throw error;
967
- }
968
- return detail;
969
- }),
970
- );
1011
+ this._log?.debug(`Removing bookmark: ${bookmarkId}, ref: ${ref}`);
971
1012
 
972
- // execute the delete bookmark action when the event is not canceled
973
- subscriber.add(
974
- dispatch$.subscribe({
975
- error: (error) => subscriber.error(error),
976
- next: (bookmark) => {
977
- // request the store to delete the bookmark
978
- this.#store.next(bookmarkActions.deleteBookmark(bookmark.id, { ref }));
979
- },
980
- }),
981
- );
1013
+ // bookmark to delete
1014
+ const bookmark = bookmarkSelector(this.#store.value, bookmarkId) ?? {
1015
+ id: bookmarkId,
1016
+ };
982
1017
 
983
- // monitor the failure case when deleting a bookmark
984
- const failure$ = action$.pipe(
985
- filter(bookmarkActions.deleteBookmark.failure.match),
986
- map(({ payload: cause }) => {
987
- const error = new BookmarkProviderError(`Failed to update bookmark: ${bookmarkId}`, {
988
- cause,
989
- });
1018
+ // observable that dispatches the 'onBookmarkDelete' event and maps the result
1019
+ const dispatch$ = from(
1020
+ this._dispatchEvent('onBookmarkDelete', {
1021
+ detail: bookmark,
1022
+ cancelable: true,
1023
+ }),
1024
+ ).pipe(
1025
+ map(({ canceled, type, detail }) => {
1026
+ if (canceled) {
1027
+ const error = new BookmarkProviderError(
1028
+ `event: ${type} was canceled by listener for removing bookmark: ${bookmarkId}, ref: ${ref}`,
1029
+ );
990
1030
  this._log?.warn(error.message);
991
1031
  throw error;
992
- }),
993
- timeout({
994
- each: defaultTimeout,
995
- with: () => {
996
- throw new BookmarkProviderError(`Timeout while updating bookmark: ${bookmarkId}`);
997
- },
998
- }),
999
- );
1032
+ }
1033
+ return detail;
1034
+ }),
1035
+ );
1000
1036
 
1001
- // monitor the success case when deleting a bookmark
1002
- const request$ = action$.pipe(
1003
- filter(bookmarkActions.deleteBookmark.success.match),
1004
- map(() => undefined),
1005
- tap(() => {
1006
- this._log?.info(`Removed bookmark: ${bookmark.id}, ref: ${ref}`);
1007
- this._dispatchEvent('onBookmarkDeleted', { detail: bookmark });
1008
- }),
1009
- raceWith(failure$),
1010
- first(),
1011
- );
1037
+ // Start the deletion process immediately
1038
+ const deleteSubscription = dispatch$.subscribe({
1039
+ error: (error) => {
1040
+ this._log?.error(`Failed to prepare bookmark deletion: ${bookmarkId}`, error);
1041
+ },
1042
+ next: (bookmark) => {
1043
+ // request the store to delete the bookmark
1044
+ this.#store.next(bookmarkActions.deleteBookmark(bookmark.id, { ref }));
1045
+ },
1046
+ });
1012
1047
 
1013
- // emit the deleted bookmark
1048
+ // monitor the failure case when deleting a bookmark
1049
+ const failure$ = action$.pipe(
1050
+ filter(bookmarkActions.deleteBookmark.failure.match),
1051
+ map(({ payload: cause }) => {
1052
+ const error = new BookmarkProviderError(`Failed to delete bookmark: ${bookmarkId}`, {
1053
+ cause,
1054
+ });
1055
+ this._log?.warn(error.message);
1056
+ throw error;
1057
+ }),
1058
+ timeout({
1059
+ each: defaultTimeout,
1060
+ with: () => {
1061
+ throw new BookmarkProviderError(`Timeout while deleting bookmark: ${bookmarkId}`);
1062
+ },
1063
+ }),
1064
+ );
1065
+
1066
+ // monitor the success case when deleting a bookmark
1067
+ const request$ = action$.pipe(
1068
+ filter(bookmarkActions.deleteBookmark.success.match),
1069
+ map(() => undefined),
1070
+ tap(() => {
1071
+ this._log?.info(`Removed bookmark: ${bookmark.id}, ref: ${ref}`);
1072
+ this._dispatchEvent('onBookmarkDeleted', { detail: bookmark });
1073
+ }),
1074
+ raceWith(failure$),
1075
+ first(),
1076
+ );
1077
+
1078
+ // Return the observable that will emit the result
1079
+ return new Observable<void>((subscriber) => {
1080
+ // Clean up the delete subscription when the result observable is unsubscribed
1081
+ subscriber.add(() => deleteSubscription.unsubscribe());
1082
+
1083
+ // Emit the deletion result
1014
1084
  request$.subscribe(subscriber);
1015
1085
  });
1016
1086
  }
@@ -1198,7 +1268,7 @@ export class BookmarkProvider implements IBookmarkProvider {
1198
1268
  this._log?.info(`Removed bookmark: ${bookmarkId} from favourites, ref: ${ref}`);
1199
1269
  this._dispatchEvent('onBookmarkFavouriteRemoved', { detail: bookmark });
1200
1270
  }),
1201
- map(() => {}),
1271
+ map(() => undefined),
1202
1272
  raceWith(failure$),
1203
1273
  first(),
1204
1274
  );
@@ -1290,7 +1360,7 @@ export class BookmarkProvider implements IBookmarkProvider {
1290
1360
  return success$.pipe(raceWith(failure$));
1291
1361
  }
1292
1362
 
1293
- protected _getBookmarkData<T extends BookmarkData>(
1363
+ protected _getBookmarkData<T extends BookmarkData = BookmarkData>(
1294
1364
  bookmarkId: string,
1295
1365
  options?: { timeout?: number },
1296
1366
  ): Observable<T | null> {
@@ -1364,16 +1434,16 @@ export class BookmarkProvider implements IBookmarkProvider {
1364
1434
  /**
1365
1435
  * Generates a unique identifier for the operation
1366
1436
  */
1367
- ref ??= generateGUID();
1437
+ const operationRef = ref ?? generateGUID();
1368
1438
 
1369
1439
  /**
1370
1440
  * Observable stream of actions filtered by a specific reference.
1371
1441
  */
1372
1442
  const action$ = this.#store.action$.pipe(
1373
- filter((action): action is TAction => 'meta' in action && action.meta?.ref === ref),
1443
+ filter((action): action is TAction => 'meta' in action && action.meta?.ref === operationRef),
1374
1444
  );
1375
1445
 
1376
- return { ref, action$ };
1446
+ return { ref: operationRef, action$ };
1377
1447
  }
1378
1448
 
1379
1449
  /**