@equinor/fusion-framework-module-bookmark 2.2.1 → 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,21 +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
413
  mergeScan(
411
- (acc, generator) => of(this._producePayload(acc, generator)),
414
+ (acc, generator) => from(Promise.resolve(this._producePayload(acc, generator))),
412
415
  initial ?? ({} as Partial<T>),
413
416
  1,
414
417
  ),
415
- tap((payload) => this._log?.debug(`generated payload`, { initial, payload })),
418
+ tap((payload) => this._log?.debug('generated payload', { initial, payload })),
416
419
  ) as Observable<T | null | undefined>;
417
420
 
418
421
  return result$;
@@ -432,35 +435,41 @@ export class BookmarkProvider implements IBookmarkProvider {
432
435
  * If the generator returns a value, a warning will be logged.
433
436
  * If the generator returns null, an info message will be logged indicating that the bookmark data should be cleared.
434
437
  */
435
- protected _producePayload<T extends BookmarkData>(
438
+ protected async _producePayload<T extends BookmarkData = BookmarkData>(
436
439
  value: Partial<T>,
437
440
  generator: BookmarkPayloadGenerator<T>,
438
441
  initial?: Partial<T> | null,
439
- ) {
440
- // produce payload from generator
441
- return produce(value, (draft) => {
442
- try {
443
- const result = generator(draft as Partial<T>, initial);
444
- // cast the result to a draft object
445
- if (result) {
446
- this._log?.warn(
447
- `bookmark data generator ${generator.name} returned a value, but it should not do this, since the data is an immer draft object`,
448
- );
449
- // Dirty fix since some developers are returning the reference object, which will freeze the object
450
- return castDraft(JSON.parse(JSON.stringify(result)));
451
- }
442
+ ): Promise<T | null | undefined> {
443
+ // Create a draft for async operations using createDraft/finishDraft pattern
444
+ const draft = createDraft(value);
452
445
 
453
- // clear the bookmark data if the generator returns null
454
- if (result === null) {
455
- this._log?.info(
456
- `bookmark data generator ${generator.name} wish to clear the bookmark data`,
457
- );
458
- return undefined;
459
- }
460
- } catch (error) {
461
- 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;
462
456
  }
463
- });
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;
464
473
  }
465
474
 
466
475
  /**
@@ -470,10 +479,10 @@ export class BookmarkProvider implements IBookmarkProvider {
470
479
  * @param options An optional object that allows excluding the bookmark payload from the result.
471
480
  * @returns An observable that emits the combined result of fetching the bookmark and bookmark data.
472
481
  */
473
- public getBookmark<T extends BookmarkData>(
482
+ public getBookmark<T extends BookmarkData = BookmarkData>(
474
483
  bookmarkId: string,
475
484
  options?: { excludePayload?: boolean },
476
- ) {
485
+ ): Observable<Bookmark<T>> {
477
486
  this._log?.debug(`fetching bookmark: ${bookmarkId}`, options);
478
487
 
479
488
  // fetch the bookmark and bookmark data
@@ -501,7 +510,7 @@ export class BookmarkProvider implements IBookmarkProvider {
501
510
  * @param options.excludePayload - Specifies whether to exclude the payload from the bookmark.
502
511
  * @returns A promise that resolves to the retrieved bookmark, or null if the bookmark is not found.
503
512
  */
504
- public getBookmarkAsync<T extends BookmarkData>(
513
+ public getBookmarkAsync<T extends BookmarkData = BookmarkData>(
505
514
  id: string,
506
515
  options?: { excludePayload?: boolean },
507
516
  ): Promise<Bookmark<T> | null> {
@@ -509,24 +518,59 @@ export class BookmarkProvider implements IBookmarkProvider {
509
518
  }
510
519
 
511
520
  /**
512
- * Asynchronously retrieves all bookmarks from the store.
521
+ * Retrieves all bookmarks from the store as an Observable stream.
513
522
  *
514
- * @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
515
552
  */
516
553
  public getAllBookmarks(): Observable<Bookmarks> {
517
554
  return new Observable<Bookmarks>((observer) => {
518
- this._log?.debug(`fetching all bookmarks`);
555
+ this._log?.debug('fetching all bookmarks');
519
556
 
520
- // generate the filter parameters
557
+ // Step 1: Generate filter parameters based on configuration
558
+ // This creates a stream that resolves all necessary filter values
521
559
  const filter$ = forkJoin({
560
+ // Always include the source system for the current provider
522
561
  sourceSystem: of(this.sourceSystem),
562
+
563
+ // Resolve application key if filtering by application is enabled
523
564
  appKey: this.#config.filters?.application
524
- ? from(this._resolve.application()).pipe(map((x) => x?.appKey))
565
+ ? defer(() => this._resolve.application()).pipe(map((x) => x?.appKey))
525
566
  : of(undefined),
567
+
568
+ // Resolve context ID if filtering by context is enabled
526
569
  contextId: this.#config.filters?.context
527
- ? from(this._resolve.context()).pipe(map((x) => x?.id))
570
+ ? defer(() => this._resolve.context()).pipe(map((x) => x?.id))
528
571
  : of(undefined),
529
572
  }).pipe(
573
+ // Handle any errors during filter parameter resolution
530
574
  catchError((err) => {
531
575
  const error = new BookmarkProviderError(
532
576
  'Could not fetch bookmarks, failed to resolve filter parameters',
@@ -537,20 +581,25 @@ export class BookmarkProvider implements IBookmarkProvider {
537
581
  }),
538
582
  );
539
583
 
540
- // 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
541
588
  observer.add(
542
589
  filter$.subscribe((filter) => {
543
590
  this.#store.next(bookmarkActions.fetchBookmarks(filter));
544
591
  }),
545
592
  );
546
593
 
547
- // 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
548
596
  const failure$ = this.#store.action$.pipe(
549
597
  filter(bookmarkActions.fetchBookmarks.failure.match),
550
598
  map((action) => {
551
599
  this._log?.error('Failed to fetch bookmarks', action.payload);
552
600
  throw new BookmarkProviderError('Failed to fetch bookmarks', action.payload);
553
601
  }),
602
+ // Add timeout protection to prevent hanging requests
554
603
  timeout({
555
604
  each: defaultTimeout,
556
605
  with: () => {
@@ -559,17 +608,22 @@ export class BookmarkProvider implements IBookmarkProvider {
559
608
  }),
560
609
  );
561
610
 
562
- // 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
563
613
  const request$ = this.#store.action$.pipe(
564
614
  filter(bookmarkActions.fetchBookmarks.success.match),
565
615
  map((a) => a.payload),
566
616
  tap((bookmarks) => {
567
- this._log?.debug(`fetched all bookmarks`, bookmarks);
617
+ this._log?.debug('fetched all bookmarks', bookmarks);
568
618
  }),
619
+ // Race against failure stream - whichever completes first wins
620
+ // This ensures we either get the bookmarks data or an error, not both
569
621
  raceWith(failure$),
622
+ // Only take the first emission (success or failure) to complete the stream
570
623
  first(),
571
624
  );
572
625
 
626
+ // Step 5: Subscribe to the request stream and forward results to observer
573
627
  request$.subscribe(observer);
574
628
  });
575
629
  }
@@ -594,7 +648,7 @@ export class BookmarkProvider implements IBookmarkProvider {
594
648
  * @returns An observable that emits the next bookmark or null.
595
649
  * @template T - The type of the bookmark data.
596
650
  */
597
- public setCurrentBookmark<T extends BookmarkData>(
651
+ public setCurrentBookmark<T extends BookmarkData = BookmarkData>(
598
652
  bookmark_or_id: Bookmark<T> | string | null,
599
653
  ): Observable<Bookmark<T> | null> {
600
654
  this._log?.debug('setting current bookmark', bookmark_or_id);
@@ -646,7 +700,7 @@ export class BookmarkProvider implements IBookmarkProvider {
646
700
  * @param bookmarkId - The ID of the bookmark to set as the current bookmark.
647
701
  * @returns A subscription to the operation that sets the current bookmark.
648
702
  */
649
- public setCurrentBookmarkAsync<T extends BookmarkData>(
703
+ public setCurrentBookmarkAsync<T extends BookmarkData = BookmarkData>(
650
704
  bookmark_or_id: Bookmark<T> | string | null,
651
705
  ): Promise<Bookmark<T> | null> {
652
706
  return lastValueFrom(this.setCurrentBookmark<T>(bookmark_or_id));
@@ -654,116 +708,121 @@ export class BookmarkProvider implements IBookmarkProvider {
654
708
 
655
709
  /**
656
710
  * Creates a new bookmark with the provided bookmark data.
711
+ * The creation executes immediately when this method is called.
657
712
  * @template T - The type of bookmark data.
658
713
  * @param {BookmarkCreateArgs<T>} newBookmarkData - The data for creating the bookmark.
659
714
  * @returns {Observable<Bookmark<T>>} - An observable that emits the created bookmark.
660
715
  */
661
- public createBookmark<T extends BookmarkData>(
716
+ public createBookmark<T extends BookmarkData = BookmarkData>(
662
717
  newBookmarkData: BookmarkCreateArgs<T>,
663
718
  ): Observable<Bookmark<T>> {
664
- return new Observable<Bookmark<T>>((subscriber) => {
665
- 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
+ );
666
756
 
667
- this._log?.debug(`creating new bookmark, ref: ${ref}`, newBookmarkData);
668
-
669
- // resolve the bookmark
670
- const bookmark$ = forkJoin({
671
- appKey: newBookmarkData.appKey
672
- ? of(newBookmarkData.appKey)
673
- : from(this._resolve.application()).pipe(
674
- map((app) => {
675
- if (!app?.appKey) {
676
- throw new BookmarkProviderError('Failed to resolve application key');
677
- }
678
- return app.appKey;
679
- }),
680
- ),
681
- contextId: this._resolve.context().then((context) => context?.id),
682
- payload: this.generatePayload<T>(newBookmarkData.payload),
683
- sourceSystem: of(this.sourceSystem),
684
- }).pipe(
685
- 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) {
686
768
  const error = new BookmarkProviderError(
687
- 'Could not create new bookmark, failed to resolve bookmark data',
688
- err,
769
+ `event: ${type} was canceled by listener for creating bookmark: ${ref}`,
689
770
  );
690
- this._log?.error(error.message, error);
771
+ this._log?.info(error.message);
691
772
  throw error;
692
- }),
693
- // merge the resolved data with the new bookmark data
694
- map(
695
- (resolvedData) =>
696
- ({
697
- ...newBookmarkData,
698
- ...resolvedData,
699
- }) as BookmarkNew<T>,
700
- ),
701
- );
702
-
703
- // notify listeners that a bookmark is about to be created
704
- const dispatch$ = bookmark$.pipe(
705
- switchMap(async (bookmark) => {
706
- // notify listeners that a bookmark is about to be created
707
- const { canceled, type } = await this._dispatchEvent('onBookmarkCreate', {
708
- detail: bookmark,
709
- cancelable: true,
710
- });
773
+ }
711
774
 
712
- // throw an error if the event is canceled
713
- if (canceled) {
714
- const error = new BookmarkProviderError(
715
- `event: ${type} was canceled by listener for creating bookmark: ${ref}`,
716
- );
717
- this._log?.info(error.message);
718
- throw error;
719
- }
775
+ return bookmark;
776
+ }),
777
+ );
720
778
 
721
- return bookmark;
722
- }),
723
- );
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
+ });
724
789
 
725
- // execute the create bookmark action when the bookmark is resolved and not canceled
726
- subscriber.add(
727
- dispatch$.subscribe({
728
- error: (error) => subscriber.error(error),
729
- next: (newBookmark) => {
730
- // request the store to create the bookmark
731
- this.#store.next(bookmarkActions.createBookmark(newBookmark, { ref }));
732
- },
733
- }),
734
- );
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
+ );
735
807
 
736
- // monitor the failure case when creating a bookmark
737
- const failure$ = action$.pipe(
738
- filter(bookmarkActions.createBookmark.failure.match),
739
- map(({ payload: cause }) => {
740
- const error = new BookmarkProviderError(`Failed to create bookmark: ${ref}`, {
741
- cause,
742
- });
743
- this._log?.warn(error.message);
744
- throw error;
745
- }),
746
- timeout({
747
- each: defaultTimeout,
748
- with: () => {
749
- throw new BookmarkProviderError(`Timeout while creating bookmark: ${ref}`);
750
- },
751
- }),
752
- );
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
+ );
753
819
 
754
- // monitor the success case when creating a bookmark
755
- const request$ = action$.pipe(
756
- filter(bookmarkActions.createBookmark.success.match),
757
- map(({ payload }) => payload as Bookmark<T>),
758
- tap((bookmark) => {
759
- this._log?.info(`Bookmark created: ${bookmark.id}, ref: ${ref}`);
760
- this._dispatchEvent('onBookmarkCreated', { detail: bookmark });
761
- }),
762
- raceWith(failure$),
763
- first(),
764
- );
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());
765
824
 
766
- // emit the created bookmark
825
+ // Emit the created bookmark
767
826
  request$.subscribe(subscriber);
768
827
  });
769
828
  }
@@ -774,7 +833,7 @@ export class BookmarkProvider implements IBookmarkProvider {
774
833
  * @param bookmark - The new bookmark to create.
775
834
  * @returns A promise that resolves to the created bookmark with its associated data.
776
835
  */
777
- public createBookmarkAsync<T extends BookmarkData>(
836
+ public createBookmarkAsync<T extends BookmarkData = BookmarkData>(
778
837
  args: BookmarkCreateArgs<T>,
779
838
  ): Promise<Bookmark<T>> {
780
839
  return lastValueFrom(this.createBookmark<T>(args));
@@ -782,14 +841,15 @@ export class BookmarkProvider implements IBookmarkProvider {
782
841
 
783
842
  /**
784
843
  * Updates a bookmark with the specified bookmarkId and bookmarkUpdates.
844
+ * The update executes immediately when this method is called.
785
845
  *
786
846
  * @template T - The type of the bookmark data.
787
847
  * @param {string} bookmarkId - The identifier of the bookmark to update.
788
848
  * @param {BookmarkUpdate<T>} [bookmarkUpdates] - The updates to apply to the bookmark.
789
849
  * @param {BookmarkUpdateOptions} [options] - The options for updating the bookmark.
790
- * @returns {Promise<Bookmark<T>>} - A promise that resolves to the updated bookmark.
850
+ * @returns {Observable<Bookmark<T>>} - An observable that emits the updated bookmark.
791
851
  */
792
- public updateBookmark<T extends BookmarkData>(
852
+ public updateBookmark<T extends BookmarkData = BookmarkData>(
793
853
  bookmarkId: string,
794
854
  bookmarkUpdates?: BookmarkUpdate<T>,
795
855
  options?: BookmarkUpdateOptions,
@@ -800,101 +860,105 @@ export class BookmarkProvider implements IBookmarkProvider {
800
860
  );
801
861
  }
802
862
 
803
- return new Observable<Bookmark<T>>((subscriber) => {
804
- const { ref, action$ } = this._useScopedActions();
863
+ const { ref, action$ } = this._useScopedActions();
805
864
 
806
- this._log?.debug(`Updating bookmark: ${bookmarkId}, ref: ${ref}`);
807
-
808
- /**
809
- * Generate updates with payload
810
- * @remarks
811
- * If `excludePayloadGeneration` is `true`, it emits the `bookmarkUpdates` directly.
812
- * If `excludePayloadGeneration` is `false`, it generates the payload using the `generatePayload` method and emits the updated `bookmarkUpdates` with the generated payload.
813
- */
814
- const updates$ = options?.excludePayloadGeneration
815
- ? of(bookmarkUpdates as BookmarkUpdate<T>)
816
- : this.generatePayload<T>(bookmarkUpdates?.payload).pipe(
817
- map(
818
- (payload) =>
819
- ({
820
- ...bookmarkUpdates,
821
- payload,
822
- }) as BookmarkUpdate<T>,
823
- ),
824
- );
865
+ this._log?.debug(`Updating bookmark: ${bookmarkId}, ref: ${ref}`);
825
866
 
826
- // notify listeners that a bookmark is about to be updated
827
- const dispatch$ = updates$.pipe(
828
- switchMap(async (updates) => {
829
- const { canceled, type } = await this._dispatchEvent('onBookmarkUpdate', {
830
- detail: {
831
- current: bookmarkSelector(this.#store.value, bookmarkId),
832
- updates,
833
- },
834
- cancelable: true,
835
- });
836
-
837
- if (canceled) {
838
- const error = new BookmarkProviderError(
839
- `event: ${type} was canceled by listener for updating bookmark: ${bookmarkId}, ref: ${ref}`,
840
- );
841
- this._log?.warn(error.message, updates);
842
- throw error;
843
- }
844
-
845
- return updates;
846
- }),
847
- );
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
+ );
848
884
 
849
- // execute the update bookmark action when the updates are resolved and not canceled
850
- subscriber.add(
851
- dispatch$.subscribe({
852
- error: (error) => subscriber.error(error),
853
- next: (updates) => {
854
- // trigger the store to update the bookmark
855
- 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,
856
892
  },
857
- }),
858
- );
893
+ cancelable: true,
894
+ });
859
895
 
860
- // monitor the failure case when updating a bookmark
861
- const failure$ = action$.pipe(
862
- filter(bookmarkActions.updateBookmark.failure.match),
863
- map(({ payload: cause }) => {
864
- const error = new BookmarkProviderError(`Failed to update bookmark: ${bookmarkId}`, {
865
- cause,
866
- });
867
- 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);
868
901
  throw error;
869
- }),
870
- timeout({
871
- each: defaultTimeout,
872
- with: () => {
873
- throw new BookmarkProviderError(`Timeout while updating bookmark: ${bookmarkId}`);
874
- },
875
- }),
876
- );
902
+ }
877
903
 
878
- // monitor the success case when updating a bookmark
879
- const request$ = action$.pipe(
880
- filter(bookmarkActions.updateBookmark.success.match),
881
- map(
882
- // TODO: add payload if current bookmark is the same as the updated bookmark
883
- ({ payload }): Bookmark<T> =>
884
- ({
885
- ...bookmarkSelector(this.#store.value, payload.id),
886
- payload: payload.payload,
887
- }) as Bookmark<T>,
888
- ),
889
- tap((bookmark) => {
890
- this._log?.info(`Bookmark updated: ${bookmark.id}, ref: ${ref}`);
891
- this._dispatchEvent('onBookmarkUpdated', { detail: bookmark });
892
- }),
893
- raceWith(failure$),
894
- first(),
895
- );
904
+ return updates;
905
+ }),
906
+ );
907
+
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());
896
960
 
897
- // emit the updated bookmark
961
+ // Emit the updated bookmark
898
962
  request$.subscribe(subscriber);
899
963
  });
900
964
  }
@@ -907,7 +971,7 @@ export class BookmarkProvider implements IBookmarkProvider {
907
971
  * @param bookmark - The bookmark to update.
908
972
  * @returns A promise that resolves to the updated bookmark with its associated data.
909
973
  */
910
- public updateBookmarkAsync<T extends BookmarkData>(
974
+ public updateBookmarkAsync<T extends BookmarkData = BookmarkData>(
911
975
  id_or_bookmark: string | Bookmark<T>,
912
976
  updates_or_options?: BookmarkUpdate<T> | BookmarkUpdateOptions,
913
977
  options?: BookmarkUpdateOptions,
@@ -925,10 +989,7 @@ export class BookmarkProvider implements IBookmarkProvider {
925
989
  updatePayload: boolean;
926
990
  }
927
991
  ).updatePayload,
928
- }).pipe(
929
- // this is totally wrong, but we need to keep the API for now
930
- map((bookmark) => bookmark.payload as Bookmark<T>),
931
- ),
992
+ }),
932
993
  );
933
994
  }
934
995
  return lastValueFrom(
@@ -938,83 +999,88 @@ export class BookmarkProvider implements IBookmarkProvider {
938
999
 
939
1000
  /**
940
1001
  * Deletes a bookmark with the specified bookmarkId.
1002
+ * The deletion executes immediately when this method is called.
941
1003
  *
942
1004
  * @param bookmarkId - The unique identifier of the bookmark to be deleted.
943
- * @returns A Promise that resolves when the bookmark is successfully deleted.
1005
+ * @returns An Observable that emits when the bookmark is successfully deleted.
944
1006
  * @throws {BookmarkProviderError} If there is an error deleting the bookmark.
945
1007
  */
946
1008
  public deleteBookmark(bookmarkId: string): Observable<void> {
947
- return new Observable<void>((subscriber) => {
948
- const { ref, action$ } = this._useScopedActions();
1009
+ const { ref, action$ } = this._useScopedActions();
949
1010
 
950
- this._log?.debug(`Removing bookmark: ${bookmarkId}, ref: ${ref}`);
1011
+ this._log?.debug(`Removing bookmark: ${bookmarkId}, ref: ${ref}`);
951
1012
 
952
- // bookmark to delete
953
- const bookmark = bookmarkSelector(this.#store.value, bookmarkId) ?? {
954
- id: bookmarkId,
955
- };
956
-
957
- // observable that dispatches the 'onBookmarkDelete' event and maps the result
958
- const dispatch$ = from(
959
- this._dispatchEvent('onBookmarkDelete', {
960
- detail: bookmark,
961
- cancelable: true,
962
- }),
963
- ).pipe(
964
- map(({ canceled, type, detail }) => {
965
- if (canceled) {
966
- const error = new BookmarkProviderError(
967
- `event: ${type} was canceled by listener for removing bookmark: ${bookmarkId}, ref: ${ref}`,
968
- );
969
- this._log?.warn(error.message);
970
- throw error;
971
- }
972
- return detail;
973
- }),
974
- );
975
-
976
- // execute the delete bookmark action when the event is not canceled
977
- subscriber.add(
978
- dispatch$.subscribe({
979
- error: (error) => subscriber.error(error),
980
- next: (bookmark) => {
981
- // request the store to delete the bookmark
982
- this.#store.next(bookmarkActions.deleteBookmark(bookmark.id, { ref }));
983
- },
984
- }),
985
- );
1013
+ // bookmark to delete
1014
+ const bookmark = bookmarkSelector(this.#store.value, bookmarkId) ?? {
1015
+ id: bookmarkId,
1016
+ };
986
1017
 
987
- // monitor the failure case when deleting a bookmark
988
- const failure$ = action$.pipe(
989
- filter(bookmarkActions.deleteBookmark.failure.match),
990
- map(({ payload: cause }) => {
991
- const error = new BookmarkProviderError(`Failed to update bookmark: ${bookmarkId}`, {
992
- cause,
993
- });
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
+ );
994
1030
  this._log?.warn(error.message);
995
1031
  throw error;
996
- }),
997
- timeout({
998
- each: defaultTimeout,
999
- with: () => {
1000
- throw new BookmarkProviderError(`Timeout while updating bookmark: ${bookmarkId}`);
1001
- },
1002
- }),
1003
- );
1032
+ }
1033
+ return detail;
1034
+ }),
1035
+ );
1004
1036
 
1005
- // monitor the success case when deleting a bookmark
1006
- const request$ = action$.pipe(
1007
- filter(bookmarkActions.deleteBookmark.success.match),
1008
- map(() => undefined),
1009
- tap(() => {
1010
- this._log?.info(`Removed bookmark: ${bookmark.id}, ref: ${ref}`);
1011
- this._dispatchEvent('onBookmarkDeleted', { detail: bookmark });
1012
- }),
1013
- raceWith(failure$),
1014
- first(),
1015
- );
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
+ });
1016
1047
 
1017
- // 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
1018
1084
  request$.subscribe(subscriber);
1019
1085
  });
1020
1086
  }
@@ -1202,7 +1268,7 @@ export class BookmarkProvider implements IBookmarkProvider {
1202
1268
  this._log?.info(`Removed bookmark: ${bookmarkId} from favourites, ref: ${ref}`);
1203
1269
  this._dispatchEvent('onBookmarkFavouriteRemoved', { detail: bookmark });
1204
1270
  }),
1205
- map(() => {}),
1271
+ map(() => undefined),
1206
1272
  raceWith(failure$),
1207
1273
  first(),
1208
1274
  );
@@ -1294,7 +1360,7 @@ export class BookmarkProvider implements IBookmarkProvider {
1294
1360
  return success$.pipe(raceWith(failure$));
1295
1361
  }
1296
1362
 
1297
- protected _getBookmarkData<T extends BookmarkData>(
1363
+ protected _getBookmarkData<T extends BookmarkData = BookmarkData>(
1298
1364
  bookmarkId: string,
1299
1365
  options?: { timeout?: number },
1300
1366
  ): Observable<T | null> {
@@ -1368,16 +1434,16 @@ export class BookmarkProvider implements IBookmarkProvider {
1368
1434
  /**
1369
1435
  * Generates a unique identifier for the operation
1370
1436
  */
1371
- ref ??= generateGUID();
1437
+ const operationRef = ref ?? generateGUID();
1372
1438
 
1373
1439
  /**
1374
1440
  * Observable stream of actions filtered by a specific reference.
1375
1441
  */
1376
1442
  const action$ = this.#store.action$.pipe(
1377
- 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),
1378
1444
  );
1379
1445
 
1380
- return { ref, action$ };
1446
+ return { ref: operationRef, action$ };
1381
1447
  }
1382
1448
 
1383
1449
  /**