@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.
- package/CHANGELOG.md +23 -0
- package/dist/esm/BookmarkConfigurator.js +6 -3
- package/dist/esm/BookmarkConfigurator.js.map +1 -1
- package/dist/esm/BookmarkProvider.js +287 -219
- package/dist/esm/BookmarkProvider.js.map +1 -1
- package/dist/esm/bookmark-config.schema.js +5 -4
- package/dist/esm/bookmark-config.schema.js.map +1 -1
- package/dist/esm/bookmark.schemas.js +4 -1
- package/dist/esm/bookmark.schemas.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/BookmarkProvider.actions.d.ts +7 -175
- package/dist/types/BookmarkProvider.d.ts +47 -46
- package/dist/types/BookmarkProvider.reducer.d.ts +7 -90
- package/dist/types/bookmark-config.schema.d.ts +13 -104
- package/dist/types/bookmark.schemas.d.ts +20 -376
- package/dist/types/types.d.ts +96 -18
- package/dist/types/version.d.ts +1 -1
- package/package.json +5 -5
- package/src/BookmarkConfigurator.ts +10 -5
- package/src/BookmarkProvider.ts +373 -307
- package/src/bookmark-config.schema.ts +7 -6
- package/src/bookmark.schemas.ts +6 -2
- package/src/types.ts +92 -28
- package/src/version.ts +1 -1
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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(
|
|
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) =>
|
|
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
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
*
|
|
379
|
+
* Retrieves all bookmarks from the store as an Observable stream.
|
|
375
380
|
*
|
|
376
|
-
*
|
|
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(
|
|
381
|
-
//
|
|
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
|
-
?
|
|
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
|
-
?
|
|
425
|
+
? defer(() => this._resolve.context()).pipe(map((x) => x?.id))
|
|
389
426
|
: of(undefined),
|
|
390
|
-
}).pipe(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
}),
|
|
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
|
-
//
|
|
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(
|
|
412
|
-
}),
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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 {
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
const error = new BookmarkProviderError(`
|
|
616
|
-
|
|
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
|
-
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
})
|
|
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
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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(() =>
|
|
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
|
|
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 ===
|
|
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.
|