@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,6 +1,6 @@
1
- import { Observable, Subscription, forkJoin, from, lastValueFrom, of } from 'rxjs';
1
+ import { Observable, Subscription, forkJoin, from, lastValueFrom, of, defer } from 'rxjs';
2
2
  import { filter, switchMap, tap, map, mergeScan, timeout, raceWith, first, catchError, } from 'rxjs/operators';
3
- import { produce, castDraft } from 'immer';
3
+ import { castDraft, createDraft, finishDraft } from 'immer';
4
4
  import { v4 as generateGUID } from 'uuid';
5
5
  import deepEqual from 'fast-deep-equal/es6';
6
6
  import { FrameworkEvent, } from '@equinor/fusion-framework-module-event';
@@ -11,7 +11,8 @@ import { createBookmarkStore, } from './BookmarkProvider.store';
11
11
  import { activeBookmarkSelector, bookmarkSelector, bookmarksSelector, errorsSelector, } from './BookmarkProvider.selectors';
12
12
  import { BookmarkProviderError } from './BookmarkProvider.error';
13
13
  import { version } from './version';
14
- const defaultTimeout = 2 * 60 * 1000; // 2 minutes
14
+ // Default timeout for bookmark operations (2 minutes)
15
+ const defaultTimeout = 2 * 60 * 1000;
15
16
  /**
16
17
  * The `BookmarkProvider` class is responsible for managing bookmarks in the application.
17
18
  * It provides methods for creating, updating, and removing bookmarks, as well as managing the current bookmark and the list of bookmarks.
@@ -257,9 +258,11 @@ export class BookmarkProvider {
257
258
  on(eventName, callback) {
258
259
  if (!this._event) {
259
260
  this._log?.warn('Failed to register event listener, event provider not configured');
260
- return () => { };
261
+ return () => {
262
+ // No-op function when event provider is not configured
263
+ };
261
264
  }
262
- return this._event?.addEventListener(eventName, (event) => {
265
+ return this._event.addEventListener(eventName, (event) => {
263
266
  if (event.source === this) {
264
267
  callback(event);
265
268
  }
@@ -294,11 +297,11 @@ export class BookmarkProvider {
294
297
  * @template T - The type of the generated payload.
295
298
  */
296
299
  generatePayload(initial) {
297
- this._log?.debug(`generating payload`, initial);
300
+ this._log?.debug('generating payload', initial);
298
301
  /**
299
302
  * Observable that emits the generated payload.
300
303
  */
301
- const result$ = from(this.#payloadGenerators).pipe(mergeScan((acc, generator) => of(this._producePayload(acc, generator)), initial ?? {}, 1), tap((payload) => this._log?.debug(`generated payload`, { initial, payload })));
304
+ const result$ = from(this.#payloadGenerators).pipe(mergeScan((acc, generator) => from(Promise.resolve(this._producePayload(acc, generator))), initial ?? {}, 1), tap((payload) => this._log?.debug('generated payload', { initial, payload })));
302
305
  return result$;
303
306
  }
304
307
  /**
@@ -315,27 +318,29 @@ export class BookmarkProvider {
315
318
  * If the generator returns a value, a warning will be logged.
316
319
  * If the generator returns null, an info message will be logged indicating that the bookmark data should be cleared.
317
320
  */
318
- _producePayload(value, generator, initial) {
319
- // produce payload from generator
320
- return produce(value, (draft) => {
321
- try {
322
- const result = generator(draft, initial);
323
- // cast the result to a draft object
324
- if (result) {
325
- this._log?.warn(`bookmark data generator ${generator.name} returned a value, but it should not do this, since the data is an immer draft object`);
326
- // Dirty fix since some developers are returning the reference object, which will freeze the object
327
- return castDraft(JSON.parse(JSON.stringify(result)));
328
- }
329
- // clear the bookmark data if the generator returns null
330
- if (result === null) {
331
- this._log?.info(`bookmark data generator ${generator.name} wish to clear the bookmark data`);
332
- return undefined;
333
- }
321
+ async _producePayload(value, generator, initial) {
322
+ // Create a draft for async operations using createDraft/finishDraft pattern
323
+ const draft = createDraft(value);
324
+ try {
325
+ const generatorResult = await Promise.resolve(generator(draft, initial));
326
+ // Handle the generator result
327
+ if (generatorResult) {
328
+ this._log?.warn(`bookmark data generator ${generator.name} returned a value, but it should not do this, since the data is an immer draft object`);
329
+ // Dirty fix since some developers are returning the reference object, which will freeze the object
330
+ return castDraft(generatorResult);
334
331
  }
335
- catch (error) {
336
- this._log?.error(`Failed to produce payload using generator ${generator.name}`, error);
332
+ // clear the bookmark data if the generator returns null
333
+ if (generatorResult === null) {
334
+ this._log?.info(`bookmark data generator ${generator.name} wish to clear the bookmark data`);
335
+ return undefined;
337
336
  }
338
- });
337
+ }
338
+ catch (error) {
339
+ this._log?.error(`Failed to produce payload using generator ${generator.name}`, error);
340
+ }
341
+ // Finish the draft to get the immutable result
342
+ const result = finishDraft(draft);
343
+ return result;
339
344
  }
340
345
  /**
341
346
  * Retrieves a bookmark with the specified bookmarkId and returns an observable that emits the combined result of fetching the bookmark and bookmark data.
@@ -371,45 +376,92 @@ export class BookmarkProvider {
371
376
  return lastValueFrom(this.getBookmark(id, options));
372
377
  }
373
378
  /**
374
- * Asynchronously retrieves all bookmarks from the store.
379
+ * Retrieves all bookmarks from the store as an Observable stream.
375
380
  *
376
- * @returns A Promise that resolves to the Bookmarks object containing all bookmarks.
381
+ * This method implements a complex flow that:
382
+ * 1. Resolves filter parameters (sourceSystem, appKey, contextId) based on configuration
383
+ * 2. Dispatches a fetch action to the store with resolved filters
384
+ * 3. Monitors store actions for success/failure responses
385
+ * 4. Returns the bookmarks data or throws appropriate errors
386
+ *
387
+ * @remarks
388
+ * The method uses a reactive pattern where filter resolution and store actions are
389
+ * handled asynchronously. If filtering is enabled, it will resolve the current
390
+ * application and context to filter bookmarks accordingly.
391
+ *
392
+ * @example
393
+ * ```ts
394
+ * // Basic usage
395
+ * bookmarkProvider.getAllBookmarks().subscribe({
396
+ * next: (bookmarks) => console.log('Retrieved bookmarks:', bookmarks),
397
+ * error: (err) => console.error('Failed to fetch bookmarks:', err)
398
+ * });
399
+ *
400
+ * // With filtering enabled in config
401
+ * const config = {
402
+ * filters: { context: true, application: true }
403
+ * };
404
+ * // Will automatically filter by current context and application
405
+ * ```
406
+ *
407
+ * @returns An Observable that emits the array of bookmarks when successfully retrieved
408
+ * @throws {BookmarkProviderError} When filter resolution fails or store operations fail
409
+ * @throws {BookmarkProviderError} When a timeout occurs during the fetch operation
377
410
  */
378
411
  getAllBookmarks() {
379
412
  return new Observable((observer) => {
380
- this._log?.debug(`fetching all bookmarks`);
381
- // generate the filter parameters
413
+ this._log?.debug('fetching all bookmarks');
414
+ // Step 1: Generate filter parameters based on configuration
415
+ // This creates a stream that resolves all necessary filter values
382
416
  const filter$ = forkJoin({
417
+ // Always include the source system for the current provider
383
418
  sourceSystem: of(this.sourceSystem),
419
+ // Resolve application key if filtering by application is enabled
384
420
  appKey: this.#config.filters?.application
385
- ? from(this._resolve.application()).pipe(map((x) => x?.appKey))
421
+ ? defer(() => this._resolve.application()).pipe(map((x) => x?.appKey))
386
422
  : of(undefined),
423
+ // Resolve context ID if filtering by context is enabled
387
424
  contextId: this.#config.filters?.context
388
- ? from(this._resolve.context()).pipe(map((x) => x?.id))
425
+ ? defer(() => this._resolve.context()).pipe(map((x) => x?.id))
389
426
  : of(undefined),
390
- }).pipe(catchError((err) => {
427
+ }).pipe(
428
+ // Handle any errors during filter parameter resolution
429
+ catchError((err) => {
391
430
  const error = new BookmarkProviderError('Could not fetch bookmarks, failed to resolve filter parameters', err);
392
431
  this._log?.error(error.message, error);
393
432
  throw error;
394
433
  }));
395
- // execute the fetch bookmarks action when the filter parameters are resolved
434
+ // Step 2: Dispatch fetch action to store when filter parameters are ready
435
+ // This triggers the actual API call through the store's action system
436
+ // The store will handle the async operation and emit success/failure actions
437
+ // Using observer.add() ensures proper cleanup when the Observable is unsubscribed
396
438
  observer.add(filter$.subscribe((filter) => {
397
439
  this.#store.next(bookmarkActions.fetchBookmarks(filter));
398
440
  }));
399
- // monitor the failure case when fetching bookmarks
441
+ // Step 3: Monitor for failure responses from the store
442
+ // This stream will emit and throw an error if the fetch operation fails
400
443
  const failure$ = this.#store.action$.pipe(filter(bookmarkActions.fetchBookmarks.failure.match), map((action) => {
401
444
  this._log?.error('Failed to fetch bookmarks', action.payload);
402
445
  throw new BookmarkProviderError('Failed to fetch bookmarks', action.payload);
403
- }), timeout({
446
+ }),
447
+ // Add timeout protection to prevent hanging requests
448
+ timeout({
404
449
  each: defaultTimeout,
405
450
  with: () => {
406
451
  throw new BookmarkProviderError('Timeout while fetching bookmarks');
407
452
  },
408
453
  }));
409
- // monitor the success case when fetching bookmarks
454
+ // Step 4: Monitor for success responses from the store
455
+ // This stream will emit the bookmarks data when successfully retrieved
410
456
  const request$ = this.#store.action$.pipe(filter(bookmarkActions.fetchBookmarks.success.match), map((a) => a.payload), tap((bookmarks) => {
411
- this._log?.debug(`fetched all bookmarks`, bookmarks);
412
- }), raceWith(failure$), first());
457
+ this._log?.debug('fetched all bookmarks', bookmarks);
458
+ }),
459
+ // Race against failure stream - whichever completes first wins
460
+ // This ensures we either get the bookmarks data or an error, not both
461
+ raceWith(failure$),
462
+ // Only take the first emission (success or failure) to complete the stream
463
+ first());
464
+ // Step 5: Subscribe to the request stream and forward results to observer
413
465
  request$.subscribe(observer);
414
466
  });
415
467
  }
@@ -473,79 +525,85 @@ export class BookmarkProvider {
473
525
  }
474
526
  /**
475
527
  * Creates a new bookmark with the provided bookmark data.
528
+ * The creation executes immediately when this method is called.
476
529
  * @template T - The type of bookmark data.
477
530
  * @param {BookmarkCreateArgs<T>} newBookmarkData - The data for creating the bookmark.
478
531
  * @returns {Observable<Bookmark<T>>} - An observable that emits the created bookmark.
479
532
  */
480
533
  createBookmark(newBookmarkData) {
481
- return new Observable((subscriber) => {
482
- const { ref, action$ } = this._useScopedActions();
483
- this._log?.debug(`creating new bookmark, ref: ${ref}`, newBookmarkData);
484
- // resolve the bookmark
485
- const bookmark$ = forkJoin({
486
- appKey: newBookmarkData.appKey
487
- ? of(newBookmarkData.appKey)
488
- : from(this._resolve.application()).pipe(map((app) => {
489
- if (!app?.appKey) {
490
- throw new BookmarkProviderError('Failed to resolve application key');
491
- }
492
- return app.appKey;
493
- })),
494
- contextId: this._resolve.context().then((context) => context?.id),
495
- payload: this.generatePayload(newBookmarkData.payload),
496
- sourceSystem: of(this.sourceSystem),
497
- }).pipe(catchError((err) => {
498
- const error = new BookmarkProviderError('Could not create new bookmark, failed to resolve bookmark data', err);
499
- this._log?.error(error.message, error);
500
- throw error;
501
- }),
502
- // merge the resolved data with the new bookmark data
503
- map((resolvedData) => ({
504
- ...newBookmarkData,
505
- ...resolvedData,
506
- })));
534
+ const { ref, action$ } = this._useScopedActions();
535
+ this._log?.debug(`creating new bookmark, ref: ${ref}`, newBookmarkData);
536
+ // resolve the bookmark
537
+ const bookmark$ = forkJoin({
538
+ appKey: newBookmarkData.appKey
539
+ ? of(newBookmarkData.appKey)
540
+ : defer(() => this._resolve.application()).pipe(map((app) => {
541
+ if (!app?.appKey) {
542
+ throw new BookmarkProviderError('Failed to resolve application key');
543
+ }
544
+ return app.appKey;
545
+ })),
546
+ contextId: defer(() => this._resolve.context()).pipe(map((context) => context?.id)),
547
+ payload: this.generatePayload(newBookmarkData.payload),
548
+ sourceSystem: of(this.sourceSystem),
549
+ }).pipe(catchError((err) => {
550
+ const error = new BookmarkProviderError('Could not create new bookmark, failed to resolve bookmark data', err);
551
+ this._log?.error(error.message, error);
552
+ throw error;
553
+ }),
554
+ // merge the resolved data with the new bookmark data
555
+ map((resolvedData) => ({
556
+ ...newBookmarkData,
557
+ ...resolvedData,
558
+ })));
559
+ // notify listeners that a bookmark is about to be created
560
+ const dispatch$ = bookmark$.pipe(switchMap(async (bookmark) => {
507
561
  // notify listeners that a bookmark is about to be created
508
- const dispatch$ = bookmark$.pipe(switchMap(async (bookmark) => {
509
- // notify listeners that a bookmark is about to be created
510
- const { canceled, type } = await this._dispatchEvent('onBookmarkCreate', {
511
- detail: bookmark,
512
- cancelable: true,
513
- });
514
- // throw an error if the event is canceled
515
- if (canceled) {
516
- const error = new BookmarkProviderError(`event: ${type} was canceled by listener for creating bookmark: ${ref}`);
517
- this._log?.info(error.message);
518
- throw error;
519
- }
520
- return bookmark;
521
- }));
522
- // execute the create bookmark action when the bookmark is resolved and not canceled
523
- subscriber.add(dispatch$.subscribe({
524
- error: (error) => subscriber.error(error),
525
- next: (newBookmark) => {
526
- // request the store to create the bookmark
527
- this.#store.next(bookmarkActions.createBookmark(newBookmark, { ref }));
528
- },
529
- }));
530
- // monitor the failure case when creating a bookmark
531
- const failure$ = action$.pipe(filter(bookmarkActions.createBookmark.failure.match), map(({ payload: cause }) => {
532
- const error = new BookmarkProviderError(`Failed to create bookmark: ${ref}`, {
533
- cause,
534
- });
535
- this._log?.warn(error.message);
562
+ const { canceled, type } = await this._dispatchEvent('onBookmarkCreate', {
563
+ detail: bookmark,
564
+ cancelable: true,
565
+ });
566
+ // throw an error if the event is canceled
567
+ if (canceled) {
568
+ const error = new BookmarkProviderError(`event: ${type} was canceled by listener for creating bookmark: ${ref}`);
569
+ this._log?.info(error.message);
536
570
  throw error;
537
- }), timeout({
538
- each: defaultTimeout,
539
- with: () => {
540
- throw new BookmarkProviderError(`Timeout while creating bookmark: ${ref}`);
541
- },
542
- }));
543
- // monitor the success case when creating a bookmark
544
- const request$ = action$.pipe(filter(bookmarkActions.createBookmark.success.match), map(({ payload }) => payload), tap((bookmark) => {
545
- this._log?.info(`Bookmark created: ${bookmark.id}, ref: ${ref}`);
546
- this._dispatchEvent('onBookmarkCreated', { detail: bookmark });
547
- }), raceWith(failure$), first());
548
- // emit the created bookmark
571
+ }
572
+ return bookmark;
573
+ }));
574
+ // Start the creation process immediately
575
+ const createSubscription = dispatch$.subscribe({
576
+ error: (error) => {
577
+ this._log?.error(`Failed to prepare bookmark creation: ${ref}`, error);
578
+ },
579
+ next: (newBookmark) => {
580
+ // request the store to create the bookmark
581
+ this.#store.next(bookmarkActions.createBookmark(newBookmark, { ref }));
582
+ },
583
+ });
584
+ // monitor the failure case when creating a bookmark
585
+ const failure$ = action$.pipe(filter(bookmarkActions.createBookmark.failure.match), map(({ payload: cause }) => {
586
+ const error = new BookmarkProviderError(`Failed to create bookmark: ${ref}`, {
587
+ cause,
588
+ });
589
+ this._log?.warn(error.message);
590
+ throw error;
591
+ }), timeout({
592
+ each: defaultTimeout,
593
+ with: () => {
594
+ throw new BookmarkProviderError(`Timeout while creating bookmark: ${ref}`);
595
+ },
596
+ }));
597
+ // monitor the success case when creating a bookmark
598
+ const request$ = action$.pipe(filter(bookmarkActions.createBookmark.success.match), map(({ payload }) => payload), tap((bookmark) => {
599
+ this._log?.info(`Bookmark created: ${bookmark.id}, ref: ${ref}`);
600
+ this._dispatchEvent('onBookmarkCreated', { detail: bookmark });
601
+ }), raceWith(failure$), first());
602
+ // Return the observable that will emit the result
603
+ return new Observable((subscriber) => {
604
+ // Clean up the create subscription when the result observable is unsubscribed
605
+ subscriber.add(() => createSubscription.unsubscribe());
606
+ // Emit the created bookmark
549
607
  request$.subscribe(subscriber);
550
608
  });
551
609
  }
@@ -560,80 +618,86 @@ export class BookmarkProvider {
560
618
  }
561
619
  /**
562
620
  * Updates a bookmark with the specified bookmarkId and bookmarkUpdates.
621
+ * The update executes immediately when this method is called.
563
622
  *
564
623
  * @template T - The type of the bookmark data.
565
624
  * @param {string} bookmarkId - The identifier of the bookmark to update.
566
625
  * @param {BookmarkUpdate<T>} [bookmarkUpdates] - The updates to apply to the bookmark.
567
626
  * @param {BookmarkUpdateOptions} [options] - The options for updating the bookmark.
568
- * @returns {Promise<Bookmark<T>>} - A promise that resolves to the updated bookmark.
627
+ * @returns {Observable<Bookmark<T>>} - An observable that emits the updated bookmark.
569
628
  */
570
629
  updateBookmark(bookmarkId, bookmarkUpdates, options) {
571
630
  if (!bookmarkUpdates && options?.excludePayloadGeneration) {
572
631
  throw new BookmarkProviderError('Cannot update bookmark without updates and excludePayloadGeneration option');
573
632
  }
574
- return new Observable((subscriber) => {
575
- const { ref, action$ } = this._useScopedActions();
576
- this._log?.debug(`Updating bookmark: ${bookmarkId}, ref: ${ref}`);
577
- /**
578
- * Generate updates with payload
579
- * @remarks
580
- * If `excludePayloadGeneration` is `true`, it emits the `bookmarkUpdates` directly.
581
- * If `excludePayloadGeneration` is `false`, it generates the payload using the `generatePayload` method and emits the updated `bookmarkUpdates` with the generated payload.
582
- */
583
- const updates$ = options?.excludePayloadGeneration
584
- ? of(bookmarkUpdates)
585
- : this.generatePayload(bookmarkUpdates?.payload).pipe(map((payload) => ({
586
- ...bookmarkUpdates,
587
- payload,
588
- })));
589
- // notify listeners that a bookmark is about to be updated
590
- const dispatch$ = updates$.pipe(switchMap(async (updates) => {
591
- const { canceled, type } = await this._dispatchEvent('onBookmarkUpdate', {
592
- detail: {
593
- current: bookmarkSelector(this.#store.value, bookmarkId),
594
- updates,
595
- },
596
- cancelable: true,
597
- });
598
- if (canceled) {
599
- const error = new BookmarkProviderError(`event: ${type} was canceled by listener for updating bookmark: ${bookmarkId}, ref: ${ref}`);
600
- this._log?.warn(error.message, updates);
601
- throw error;
602
- }
603
- return updates;
604
- }));
605
- // execute the update bookmark action when the updates are resolved and not canceled
606
- subscriber.add(dispatch$.subscribe({
607
- error: (error) => subscriber.error(error),
608
- next: (updates) => {
609
- // trigger the store to update the bookmark
610
- this.#store.next(bookmarkActions.updateBookmark({ bookmarkId, updates }, { ref }));
633
+ const { ref, action$ } = this._useScopedActions();
634
+ this._log?.debug(`Updating bookmark: ${bookmarkId}, ref: ${ref}`);
635
+ /**
636
+ * Generate updates with payload
637
+ * @remarks
638
+ * If `excludePayloadGeneration` is `true`, it emits the `bookmarkUpdates` directly.
639
+ * If `excludePayloadGeneration` is `false`, it generates the payload using the `generatePayload` method and emits the updated `bookmarkUpdates` with the generated payload.
640
+ */
641
+ const updates$ = options?.excludePayloadGeneration
642
+ ? of(bookmarkUpdates)
643
+ : this.generatePayload(bookmarkUpdates?.payload).pipe(map((payload) => ({
644
+ ...bookmarkUpdates,
645
+ payload,
646
+ })));
647
+ // notify listeners that a bookmark is about to be updated
648
+ const dispatch$ = updates$.pipe(switchMap(async (updates) => {
649
+ const { canceled, type } = await this._dispatchEvent('onBookmarkUpdate', {
650
+ detail: {
651
+ current: bookmarkSelector(this.#store.value, bookmarkId),
652
+ updates,
611
653
  },
612
- }));
613
- // monitor the failure case when updating a bookmark
614
- const failure$ = action$.pipe(filter(bookmarkActions.updateBookmark.failure.match), map(({ payload: cause }) => {
615
- const error = new BookmarkProviderError(`Failed to update bookmark: ${bookmarkId}`, {
616
- cause,
617
- });
618
- this._log?.info(error.message);
654
+ cancelable: true,
655
+ });
656
+ if (canceled) {
657
+ const error = new BookmarkProviderError(`event: ${type} was canceled by listener for updating bookmark: ${bookmarkId}, ref: ${ref}`);
658
+ this._log?.warn(error.message, updates);
619
659
  throw error;
620
- }), timeout({
621
- each: defaultTimeout,
622
- with: () => {
623
- throw new BookmarkProviderError(`Timeout while updating bookmark: ${bookmarkId}`);
624
- },
625
- }));
626
- // monitor the success case when updating a bookmark
627
- const request$ = action$.pipe(filter(bookmarkActions.updateBookmark.success.match), map(
628
- // TODO: add payload if current bookmark is the same as the updated bookmark
629
- ({ payload }) => ({
630
- ...bookmarkSelector(this.#store.value, payload.id),
631
- payload: payload.payload,
632
- })), tap((bookmark) => {
633
- this._log?.info(`Bookmark updated: ${bookmark.id}, ref: ${ref}`);
634
- this._dispatchEvent('onBookmarkUpdated', { detail: bookmark });
635
- }), raceWith(failure$), first());
636
- // emit the updated bookmark
660
+ }
661
+ return updates;
662
+ }));
663
+ // Start the update process immediately
664
+ const updateSubscription = dispatch$.subscribe({
665
+ error: (error) => {
666
+ this._log?.error(`Failed to prepare bookmark update: ${bookmarkId}`, error);
667
+ },
668
+ next: (updates) => {
669
+ // trigger the store to update the bookmark
670
+ this.#store.next(bookmarkActions.updateBookmark({ bookmarkId, updates }, { ref }));
671
+ },
672
+ });
673
+ // monitor the failure case when updating a bookmark
674
+ const failure$ = action$.pipe(filter(bookmarkActions.updateBookmark.failure.match), map(({ payload: cause }) => {
675
+ const error = new BookmarkProviderError(`Failed to update bookmark: ${bookmarkId}`, {
676
+ cause,
677
+ });
678
+ this._log?.info(error.message);
679
+ throw error;
680
+ }), timeout({
681
+ each: defaultTimeout,
682
+ with: () => {
683
+ throw new BookmarkProviderError(`Timeout while updating bookmark: ${bookmarkId}`);
684
+ },
685
+ }));
686
+ // monitor the success case when updating a bookmark
687
+ const request$ = action$.pipe(filter(bookmarkActions.updateBookmark.success.match), map(
688
+ // TODO: add payload if current bookmark is the same as the updated bookmark
689
+ ({ payload }) => ({
690
+ ...bookmarkSelector(this.#store.value, payload.id),
691
+ payload: payload.payload,
692
+ })), tap((bookmark) => {
693
+ this._log?.info(`Bookmark updated: ${bookmark.id}, ref: ${ref}`);
694
+ this._dispatchEvent('onBookmarkUpdated', { detail: bookmark });
695
+ }), raceWith(failure$), first());
696
+ // Return the observable that will emit the result
697
+ return new Observable((subscriber) => {
698
+ // Clean up the update subscription when the result observable is unsubscribed
699
+ subscriber.add(() => updateSubscription.unsubscribe());
700
+ // Emit the updated bookmark
637
701
  request$.subscribe(subscriber);
638
702
  });
639
703
  }
@@ -652,66 +716,70 @@ export class BookmarkProvider {
652
716
  const { id, ...updates } = id_or_bookmark;
653
717
  return lastValueFrom(this.updateBookmark(id, updates, {
654
718
  excludePayloadGeneration: !updates_or_options.updatePayload,
655
- }).pipe(
656
- // this is totally wrong, but we need to keep the API for now
657
- map((bookmark) => bookmark.payload)));
719
+ }));
658
720
  }
659
721
  return lastValueFrom(this.updateBookmark(id_or_bookmark, updates_or_options, options));
660
722
  }
661
723
  /**
662
724
  * Deletes a bookmark with the specified bookmarkId.
725
+ * The deletion executes immediately when this method is called.
663
726
  *
664
727
  * @param bookmarkId - The unique identifier of the bookmark to be deleted.
665
- * @returns A Promise that resolves when the bookmark is successfully deleted.
728
+ * @returns An Observable that emits when the bookmark is successfully deleted.
666
729
  * @throws {BookmarkProviderError} If there is an error deleting the bookmark.
667
730
  */
668
731
  deleteBookmark(bookmarkId) {
669
- return new Observable((subscriber) => {
670
- const { ref, action$ } = this._useScopedActions();
671
- this._log?.debug(`Removing bookmark: ${bookmarkId}, ref: ${ref}`);
672
- // bookmark to delete
673
- const bookmark = bookmarkSelector(this.#store.value, bookmarkId) ?? {
674
- id: bookmarkId,
675
- };
676
- // observable that dispatches the 'onBookmarkDelete' event and maps the result
677
- const dispatch$ = from(this._dispatchEvent('onBookmarkDelete', {
678
- detail: bookmark,
679
- cancelable: true,
680
- })).pipe(map(({ canceled, type, detail }) => {
681
- if (canceled) {
682
- const error = new BookmarkProviderError(`event: ${type} was canceled by listener for removing bookmark: ${bookmarkId}, ref: ${ref}`);
683
- this._log?.warn(error.message);
684
- throw error;
685
- }
686
- return detail;
687
- }));
688
- // execute the delete bookmark action when the event is not canceled
689
- subscriber.add(dispatch$.subscribe({
690
- error: (error) => subscriber.error(error),
691
- next: (bookmark) => {
692
- // request the store to delete the bookmark
693
- this.#store.next(bookmarkActions.deleteBookmark(bookmark.id, { ref }));
694
- },
695
- }));
696
- // monitor the failure case when deleting a bookmark
697
- const failure$ = action$.pipe(filter(bookmarkActions.deleteBookmark.failure.match), map(({ payload: cause }) => {
698
- const error = new BookmarkProviderError(`Failed to update bookmark: ${bookmarkId}`, {
699
- cause,
700
- });
732
+ const { ref, action$ } = this._useScopedActions();
733
+ this._log?.debug(`Removing bookmark: ${bookmarkId}, ref: ${ref}`);
734
+ // bookmark to delete
735
+ const bookmark = bookmarkSelector(this.#store.value, bookmarkId) ?? {
736
+ id: bookmarkId,
737
+ };
738
+ // observable that dispatches the 'onBookmarkDelete' event and maps the result
739
+ const dispatch$ = from(this._dispatchEvent('onBookmarkDelete', {
740
+ detail: bookmark,
741
+ cancelable: true,
742
+ })).pipe(map(({ canceled, type, detail }) => {
743
+ if (canceled) {
744
+ const error = new BookmarkProviderError(`event: ${type} was canceled by listener for removing bookmark: ${bookmarkId}, ref: ${ref}`);
701
745
  this._log?.warn(error.message);
702
746
  throw error;
703
- }), timeout({
704
- each: defaultTimeout,
705
- with: () => {
706
- throw new BookmarkProviderError(`Timeout while updating bookmark: ${bookmarkId}`);
707
- },
708
- }));
709
- // monitor the success case when deleting a bookmark
710
- const request$ = action$.pipe(filter(bookmarkActions.deleteBookmark.success.match), map(() => undefined), tap(() => {
711
- this._log?.info(`Removed bookmark: ${bookmark.id}, ref: ${ref}`);
712
- this._dispatchEvent('onBookmarkDeleted', { detail: bookmark });
713
- }), raceWith(failure$), first());
714
- // emit the deleted bookmark
747
+ }
748
+ return detail;
749
+ }));
750
+ // Start the deletion process immediately
751
+ const deleteSubscription = dispatch$.subscribe({
752
+ error: (error) => {
753
+ this._log?.error(`Failed to prepare bookmark deletion: ${bookmarkId}`, error);
754
+ },
755
+ next: (bookmark) => {
756
+ // request the store to delete the bookmark
757
+ this.#store.next(bookmarkActions.deleteBookmark(bookmark.id, { ref }));
758
+ },
759
+ });
760
+ // monitor the failure case when deleting a bookmark
761
+ const failure$ = action$.pipe(filter(bookmarkActions.deleteBookmark.failure.match), map(({ payload: cause }) => {
762
+ const error = new BookmarkProviderError(`Failed to delete bookmark: ${bookmarkId}`, {
763
+ cause,
764
+ });
765
+ this._log?.warn(error.message);
766
+ throw error;
767
+ }), timeout({
768
+ each: defaultTimeout,
769
+ with: () => {
770
+ throw new BookmarkProviderError(`Timeout while deleting bookmark: ${bookmarkId}`);
771
+ },
772
+ }));
773
+ // monitor the success case when deleting a bookmark
774
+ const request$ = action$.pipe(filter(bookmarkActions.deleteBookmark.success.match), map(() => undefined), tap(() => {
775
+ this._log?.info(`Removed bookmark: ${bookmark.id}, ref: ${ref}`);
776
+ this._dispatchEvent('onBookmarkDeleted', { detail: bookmark });
777
+ }), raceWith(failure$), first());
778
+ // Return the observable that will emit the result
779
+ return new Observable((subscriber) => {
780
+ // Clean up the delete subscription when the result observable is unsubscribed
781
+ subscriber.add(() => deleteSubscription.unsubscribe());
782
+ // Emit the deletion result
715
783
  request$.subscribe(subscriber);
716
784
  });
717
785
  }
@@ -839,7 +907,7 @@ export class BookmarkProvider {
839
907
  const request$ = action$.pipe(filter(bookmarkActions.removeBookmarkAsFavourite.success.match), tap(() => {
840
908
  this._log?.info(`Removed bookmark: ${bookmarkId} from favourites, ref: ${ref}`);
841
909
  this._dispatchEvent('onBookmarkFavouriteRemoved', { detail: bookmark });
842
- }), map(() => { }), raceWith(failure$), first());
910
+ }), map(() => undefined), raceWith(failure$), first());
843
911
  // emit the removed bookmark
844
912
  request$.subscribe(subscriber);
845
913
  });
@@ -941,12 +1009,12 @@ export class BookmarkProvider {
941
1009
  /**
942
1010
  * Generates a unique identifier for the operation
943
1011
  */
944
- ref ??= generateGUID();
1012
+ const operationRef = ref ?? generateGUID();
945
1013
  /**
946
1014
  * Observable stream of actions filtered by a specific reference.
947
1015
  */
948
- const action$ = this.#store.action$.pipe(filter((action) => 'meta' in action && action.meta?.ref === ref));
949
- return { ref, action$ };
1016
+ const action$ = this.#store.action$.pipe(filter((action) => 'meta' in action && action.meta?.ref === operationRef));
1017
+ return { ref: operationRef, action$ };
950
1018
  }
951
1019
  /**
952
1020
  * Disposes the BookmarkProvider.