@internetarchive/ia-book-actions 1.4.4-alpha13 → 1.4.4-alpha14
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/package.json +1 -1
- package/src/ia-book-actions.js +160 -251
- package/test/ia-book-actions.test.js +171 -2
package/package.json
CHANGED
package/src/ia-book-actions.js
CHANGED
|
@@ -97,24 +97,29 @@ export default class IABookActions extends LitElement {
|
|
|
97
97
|
this.loanRenewInProgress = false;
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* True while a
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
|
|
100
|
+
* True while a create_token error modal is up and the token poller is
|
|
101
|
+
* retrying in the background. Cleared by startLoanTokenPoller()'s
|
|
102
|
+
* successCallback. See handleLendingActionError.
|
|
103
|
+
*/
|
|
104
|
+
this.awaitingTokenRecovery = false;
|
|
105
|
+
|
|
106
|
+
/** Bounds how many times handleLendingActionError restarts the poller
|
|
107
|
+
* for one outage, so a genuinely-lost loan can't retry forever. */
|
|
108
|
+
this.tokenRecoveryAttempts = 0;
|
|
109
|
+
this.maxTokenRecoveryAttempts = 3;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* True while recovering from a loan that genuinely lapsed (set by
|
|
113
|
+
* autoRenewExpiredLoan), as opposed to a routine pre-expiry top-up.
|
|
114
|
+
* Only the recovery case needs BookReader re-initialized — see
|
|
104
115
|
* handleLoanAutoRenewed.
|
|
105
116
|
*/
|
|
106
117
|
this.recoveringFromLoanExpiry = false;
|
|
107
118
|
|
|
108
119
|
this.warningModalOpen = false;
|
|
109
120
|
|
|
110
|
-
/**
|
|
111
|
-
*
|
|
112
|
-
* don't nag them again with the same modal every timer tick — only a
|
|
113
|
-
* real renewal (via interacting with the book) clears this, giving the
|
|
114
|
-
* next pre-expiry window its own single warning.
|
|
115
|
-
* @see loanRenewAttempt
|
|
116
|
-
* @see handleLoanAutoRenewed
|
|
117
|
-
*/
|
|
121
|
+
/** Once dismissed, don't re-show the warning modal every tick — only
|
|
122
|
+
* a real renewal (handleLoanAutoRenewed) re-arms it. */
|
|
118
123
|
this.warningModalDismissed = false;
|
|
119
124
|
|
|
120
125
|
/**
|
|
@@ -148,9 +153,9 @@ export default class IABookActions extends LitElement {
|
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
disconnectedCallback() {
|
|
151
|
-
|
|
156
|
+
super.disconnectedCallback();
|
|
152
157
|
window?.IALendingIntervals?.clearAll();
|
|
153
|
-
|
|
158
|
+
this.tokenPoller?.disconnectedCallback();
|
|
154
159
|
this.sentryCaptureMsg(sentryLogs.disconnectedCallback);
|
|
155
160
|
this.disconnectResizeObserver();
|
|
156
161
|
}
|
|
@@ -189,12 +194,8 @@ export default class IABookActions extends LitElement {
|
|
|
189
194
|
}
|
|
190
195
|
|
|
191
196
|
if (changed.has('loanRenewResult') && this.loanRenewResult.renewNow) {
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
// so every path (interaction during active reading, periodic
|
|
195
|
-
// checker, auto-return recovery) gets the same "renewing" signal
|
|
196
|
-
// instead of just looking frozen until handleLoanAutoRenewed() clears
|
|
197
|
-
// this and restarts the timer.
|
|
197
|
+
// Pause the countdown for the renew_loan round-trip, for every
|
|
198
|
+
// renewal path — cleared by handleLoanAutoRenewed().
|
|
198
199
|
this.loanRenewInProgress = true;
|
|
199
200
|
window.IALendingIntervals.clearAll();
|
|
200
201
|
}
|
|
@@ -269,13 +270,8 @@ export default class IABookActions extends LitElement {
|
|
|
269
270
|
'browsingExpired' in this.lendingStatus &&
|
|
270
271
|
this.lendingStatus?.browsingExpired;
|
|
271
272
|
if (hasExpired) {
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
// applies when there IS a rendered bar to preserve. On a fresh page
|
|
275
|
-
// load of an already-expired loan nothing has been computed yet, so
|
|
276
|
-
// skipping this would leave the patron staring at a blank bar with no
|
|
277
|
-
// way to re-borrow. In that case compute the normal (Borrow) actions
|
|
278
|
-
// before short-circuiting the rest of the lifecycle below.
|
|
273
|
+
// Auto-return must not visibly change the bar — unless nothing's
|
|
274
|
+
// rendered yet (fresh load of an already-expired loan).
|
|
279
275
|
if (this.primaryActions?.length) {
|
|
280
276
|
log('[IABookActions] browsing expired — leaving action bar untouched');
|
|
281
277
|
} else {
|
|
@@ -305,18 +301,15 @@ export default class IABookActions extends LitElement {
|
|
|
305
301
|
|
|
306
302
|
if (!this.applyLendingActions()) return;
|
|
307
303
|
|
|
308
|
-
// Don't (re)start the countdown
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// start timer for loan-renew
|
|
304
|
+
// Don't (re)start the countdown mid-renewal (stale secondsLeftOnLoan)
|
|
305
|
+
// or while recovering a create_token failure (timers stopped
|
|
306
|
+
// deliberately — see handleLendingActionError).
|
|
307
|
+
if (
|
|
308
|
+
this.borrowType === 'browsed' &&
|
|
309
|
+
!this.loanRenewInProgress &&
|
|
310
|
+
!this.awaitingTokenRecovery
|
|
311
|
+
) {
|
|
317
312
|
await this.startTimerCountdown();
|
|
318
|
-
|
|
319
|
-
// start timer for browsed.
|
|
320
313
|
await this.startBrowseTimer();
|
|
321
314
|
}
|
|
322
315
|
|
|
@@ -326,15 +319,8 @@ export default class IABookActions extends LitElement {
|
|
|
326
319
|
return;
|
|
327
320
|
}
|
|
328
321
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
* - if book is going to renew, need to wait until renew is completed:
|
|
332
|
-
* loanRenewInProgress is set before renew_loan is even dispatched
|
|
333
|
-
* (including the optimistic pre-confirmation window) and only
|
|
334
|
-
* clears once the renewal is confirmed, so starting the poller here
|
|
335
|
-
* mid-renewal would call create_token against the loan record
|
|
336
|
-
* before it's actually renewed.
|
|
337
|
-
*/
|
|
322
|
+
// Wait until any in-flight renewal is confirmed before (re)starting the
|
|
323
|
+
// poller, so create_token isn't called against a not-yet-renewed loan.
|
|
338
324
|
setTimeout(() => {
|
|
339
325
|
if (
|
|
340
326
|
!hasExpired &&
|
|
@@ -350,24 +336,9 @@ export default class IABookActions extends LitElement {
|
|
|
350
336
|
}
|
|
351
337
|
|
|
352
338
|
/**
|
|
353
|
-
* Is this
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
* BookReader.updateFirstIndex() fires `userAction` unconditionally, and
|
|
357
|
-
* init() reaches it via updateFromParams() — so a plain page refresh
|
|
358
|
-
* looks identical to a page turn. Auto-renewing off that would silently
|
|
359
|
-
* re-borrow a lapsed book with no patron input at all.
|
|
360
|
-
*
|
|
361
|
-
* BookReader.trigger() passes the BookReader instance as
|
|
362
|
-
* `event.detail.props`, and init() holds `init.initComplete === false`
|
|
363
|
-
* for exactly the span that contains that init-time jump, setting it to
|
|
364
|
-
* true immediately before firing PostInit. So the flag is an exact,
|
|
365
|
-
* timing-independent marker of "this came from init".
|
|
366
|
-
*
|
|
367
|
-
* Only an explicit `false` suppresses the event: a BookReader too old to
|
|
368
|
-
* expose the flag, or a synthetic event with no detail, must still be
|
|
369
|
-
* treated as a genuine interaction.
|
|
370
|
-
*
|
|
339
|
+
* Is this BookReader's own init-time jump to the last-read page, rather
|
|
340
|
+
* than a real patron interaction (which fires the identical event)? Only
|
|
341
|
+
* an explicit `false` counts; a missing flag must be treated as real.
|
|
371
342
|
* @param {Event} event - the BookReader:userAction event
|
|
372
343
|
* @returns {boolean}
|
|
373
344
|
*/
|
|
@@ -393,14 +364,9 @@ export default class IABookActions extends LitElement {
|
|
|
393
364
|
return;
|
|
394
365
|
}
|
|
395
366
|
|
|
396
|
-
// Capture before autoRenewExpiredLoan() runs — it
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
// false and wrongly let autoLoanRenewChecker() run too. That second
|
|
400
|
-
// call would then overwrite loanRenewResult.renewNow (set true by
|
|
401
|
-
// autoRenewExpiredLoan) back to false, since the loanTime cache entry
|
|
402
|
-
// was already deleted by browseHasExpired() — silently killing the
|
|
403
|
-
// in-flight renewal and leaving the countdown stuck.
|
|
367
|
+
// Capture before autoRenewExpiredLoan() runs — it optimistically
|
|
368
|
+
// flips browsingExpired to false, which would otherwise also let
|
|
369
|
+
// autoLoanRenewChecker() run and clobber the in-flight renewal.
|
|
404
370
|
const wasExpired = this.lendingStatus.browsingExpired;
|
|
405
371
|
|
|
406
372
|
log('[IABookActions] BookReader:userAction received', {
|
|
@@ -408,7 +374,6 @@ export default class IABookActions extends LitElement {
|
|
|
408
374
|
browsingExpired: wasExpired,
|
|
409
375
|
});
|
|
410
376
|
|
|
411
|
-
// If the loan expired while the tab stayed visible, auto-renew on page turn
|
|
412
377
|
if (wasExpired) {
|
|
413
378
|
this.autoRenewExpiredLoan();
|
|
414
379
|
}
|
|
@@ -418,11 +383,8 @@ export default class IABookActions extends LitElement {
|
|
|
418
383
|
}
|
|
419
384
|
});
|
|
420
385
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
* so when user open that tab again,
|
|
424
|
-
* the [visibilitychange] event can trigger timer to show relavent messages
|
|
425
|
-
*/
|
|
386
|
+
// A tab in the background can have its intervals throttled/paused, so
|
|
387
|
+
// re-check status when the patron comes back to it.
|
|
426
388
|
document.addEventListener('visibilitychange', async () => {
|
|
427
389
|
if (document.hidden) {
|
|
428
390
|
log('[IABookActions] visibilitychange: tab backgrounded');
|
|
@@ -443,6 +405,10 @@ export default class IABookActions extends LitElement {
|
|
|
443
405
|
|
|
444
406
|
if (this.borrowType !== 'browsed') return;
|
|
445
407
|
|
|
408
|
+
// Timers are deliberately stopped during create_token recovery
|
|
409
|
+
// (see handleLendingActionError) — leave them alone here.
|
|
410
|
+
if (this.awaitingTokenRecovery) return;
|
|
411
|
+
|
|
446
412
|
if (this.lendingStatus.browsingExpired === false) {
|
|
447
413
|
const loanTime = await this.localCache.get(
|
|
448
414
|
`${this.identifier}-loanTime`
|
|
@@ -454,14 +420,9 @@ export default class IABookActions extends LitElement {
|
|
|
454
420
|
if (secondsLeft >= this.timerExecutionSeconds) {
|
|
455
421
|
this.loanStatusCheckInterval(Number(secondsLeft));
|
|
456
422
|
} else {
|
|
457
|
-
// Loan expired while
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
// following modal navigated away, but the reading session now
|
|
461
|
-
// continues. Tearing down would drop the resize observer for
|
|
462
|
-
// good (it's only re-added on firstUpdated / sharedObserver
|
|
463
|
-
// change), leaving the bar unable to respond to viewport
|
|
464
|
-
// changes, and would report a bogus disconnect to Sentry.
|
|
423
|
+
// Loan expired while away — silently renew. Only clear
|
|
424
|
+
// intervals; disconnectedCallback() would drop the resize
|
|
425
|
+
// observer for a session that's still continuing.
|
|
465
426
|
window?.IALendingIntervals?.clearAll();
|
|
466
427
|
this.autoRenewExpiredLoan();
|
|
467
428
|
}
|
|
@@ -481,15 +442,11 @@ export default class IABookActions extends LitElement {
|
|
|
481
442
|
* @param {Boolean} hasPageChanged
|
|
482
443
|
*/
|
|
483
444
|
async autoLoanRenewChecker(hasPageChanged = false) {
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
// (
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
// resolve last would clobber this.loanRenewResult, potentially
|
|
490
|
-
// re-triggering the whole renew_loan/create_token flow while a
|
|
491
|
-
// renewal from an earlier event is already in flight or just landed.
|
|
492
|
-
if (this.loanRenewInProgress) return;
|
|
445
|
+
// Re-entrancy guard against rapid BookReader:userAction events, and
|
|
446
|
+
// against restarting a renewal while still recovering the last one's
|
|
447
|
+
// create_token (loanRenewInProgress clears as soon as renew_loan
|
|
448
|
+
// itself succeeds, before that's known).
|
|
449
|
+
if (this.loanRenewInProgress || this.awaitingTokenRecovery) return;
|
|
493
450
|
|
|
494
451
|
this.loanRenewHelper = new LoanRenewHelper(
|
|
495
452
|
hasPageChanged,
|
|
@@ -503,20 +460,18 @@ export default class IABookActions extends LitElement {
|
|
|
503
460
|
}
|
|
504
461
|
|
|
505
462
|
/**
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
* Reuses the same renew_loan path as the automatic loan-renew checker.
|
|
511
|
-
* On success: handleLoanAutoRenewed() resets the timer seamlessly.
|
|
512
|
-
* On error: handleLendingActionError() shows showLoanUnavailableModal().
|
|
463
|
+
* Silently attempt to renew a browse loan that has expired, on
|
|
464
|
+
* visibilitychange or a page turn. handleLoanAutoRenewed()/
|
|
465
|
+
* handleLendingActionError() handle the outcome either way.
|
|
513
466
|
*/
|
|
514
467
|
autoRenewExpiredLoan() {
|
|
515
|
-
if (this.loanRenewInProgress) {
|
|
468
|
+
if (this.loanRenewInProgress || this.awaitingTokenRecovery) {
|
|
516
469
|
log(
|
|
517
470
|
'[IABookActions] autoRenewExpiredLoan: skipped, renewal already in progress',
|
|
518
471
|
{
|
|
519
472
|
identifier: this.identifier,
|
|
473
|
+
loanRenewInProgress: this.loanRenewInProgress,
|
|
474
|
+
awaitingTokenRecovery: this.awaitingTokenRecovery,
|
|
520
475
|
}
|
|
521
476
|
);
|
|
522
477
|
return;
|
|
@@ -529,16 +484,8 @@ export default class IABookActions extends LitElement {
|
|
|
529
484
|
|
|
530
485
|
this.modal?.closeModal();
|
|
531
486
|
|
|
532
|
-
// Optimistically flip browsingExpired
|
|
533
|
-
//
|
|
534
|
-
// flight, instead of showing the blue "Borrow" state. Deliberately NOT
|
|
535
|
-
// resetting secondsLeftOnLoan here — the renewal hasn't been confirmed
|
|
536
|
-
// yet (it can take several seconds, or genuinely fail), so showing a
|
|
537
|
-
// full hour before we know the outcome would be misleading. The real
|
|
538
|
-
// value is set once handleLoanAutoRenewed() confirms success;
|
|
539
|
-
// loanRenewInProgress (already true above) is the signal consumers
|
|
540
|
-
// should use to show a "renewing" state instead of trusting the
|
|
541
|
-
// countdown during this window.
|
|
487
|
+
// Optimistically flip browsingExpired so the bar stays in the reading
|
|
488
|
+
// state during the renew_loan round-trip, instead of showing "Borrow".
|
|
542
489
|
this.lendingStatus = {
|
|
543
490
|
...this.lendingStatus,
|
|
544
491
|
browsingExpired: false,
|
|
@@ -572,7 +519,6 @@ export default class IABookActions extends LitElement {
|
|
|
572
519
|
|
|
573
520
|
log('[IABookActions] showWarningModal');
|
|
574
521
|
|
|
575
|
-
// Capture texts and secondsLeft before resetting loanRenewResult
|
|
576
522
|
const {
|
|
577
523
|
texts: warningTexts,
|
|
578
524
|
secondsLeft: rawSecondsLeft,
|
|
@@ -584,7 +530,6 @@ export default class IABookActions extends LitElement {
|
|
|
584
530
|
secondsLeft = secondsLeft > 60 ? secondsLeft : 60;
|
|
585
531
|
}
|
|
586
532
|
|
|
587
|
-
// clear modal and reset renew state
|
|
588
533
|
this.modal.customModalContent = nothing;
|
|
589
534
|
this.modal?.closeModal();
|
|
590
535
|
this.loanRenewResult = { texts: '', renewNow: false };
|
|
@@ -666,9 +611,8 @@ export default class IABookActions extends LitElement {
|
|
|
666
611
|
'This book has been returned due to inactivity.';
|
|
667
612
|
|
|
668
613
|
this.modal?.closeModal();
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
// it again. Otherwise it stays latched for the life of the component.
|
|
614
|
+
// Release the warning-modal re-entrancy guard, or it stays latched for
|
|
615
|
+
// the life of the component.
|
|
672
616
|
this.warningModalOpen = false;
|
|
673
617
|
|
|
674
618
|
this.sentryCaptureMsg(sentryLogs.browseHasExpired);
|
|
@@ -776,31 +720,16 @@ export default class IABookActions extends LitElement {
|
|
|
776
720
|
}
|
|
777
721
|
|
|
778
722
|
/**
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
* - then change the remaining time
|
|
782
|
-
* - reset timer state
|
|
723
|
+
* Runs after a loan renewal completes (successfully or not): shows the
|
|
724
|
+
* outcome, updates the remaining time, and resets renewal-in-flight state.
|
|
783
725
|
* @param {object} event
|
|
784
726
|
*/
|
|
785
727
|
async handleLoanAutoRenewed({ detail }) {
|
|
786
728
|
const activeLoan = detail?.data?.loan;
|
|
787
729
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
*
|
|
792
|
-
* A `renewal: false` loan reaching the success path below is actively
|
|
793
|
-
* harmful: setBrowseTimeSession() never ran, so the loanTime read comes
|
|
794
|
-
* back undefined, and the NaN fallback then invents a full fresh hour —
|
|
795
|
-
* putting a brand-new countdown on screen alongside the "someone else
|
|
796
|
-
* has now borrowed it" modal.
|
|
797
|
-
*
|
|
798
|
-
* Clearing loanRenewInProgress here matters just as much. Leaving it set
|
|
799
|
-
* latches the component: autoLoanRenewChecker() and
|
|
800
|
-
* autoRenewExpiredLoan() both no-op on it, and neither the countdown nor
|
|
801
|
-
* the token poller will restart — the loan sits "active" forever with a
|
|
802
|
-
* dead timer.
|
|
803
|
-
*/
|
|
730
|
+
// Treat anything but a confirmed renewal as a failure, or
|
|
731
|
+
// loanRenewInProgress stays latched forever (autoLoanRenewChecker/
|
|
732
|
+
// autoRenewExpiredLoan both no-op on it).
|
|
804
733
|
if (!activeLoan?.renewal) {
|
|
805
734
|
this.loanRenewInProgress = false;
|
|
806
735
|
this.recoveringFromLoanExpiry = false;
|
|
@@ -816,7 +745,6 @@ export default class IABookActions extends LitElement {
|
|
|
816
745
|
}
|
|
817
746
|
|
|
818
747
|
if (this.loanRenewResult.renewNow) {
|
|
819
|
-
// Now, let's reset loan duration & this.lendingStatus
|
|
820
748
|
const loanTime = await this.localCache.get(`${this.identifier}-loanTime`);
|
|
821
749
|
|
|
822
750
|
// number of seconds left in current loan
|
|
@@ -834,19 +762,8 @@ export default class IABookActions extends LitElement {
|
|
|
834
762
|
});
|
|
835
763
|
|
|
836
764
|
if (this.recoveringFromLoanExpiry) {
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
// re-initialize (create_token was minting its access cookie off
|
|
840
|
-
// the loan's expiry; doing this before confirmation risked reading
|
|
841
|
-
// the still-expired record). loanRenewInProgress already kept the
|
|
842
|
-
// token poller from restarting early, so it'll pick up fresh here
|
|
843
|
-
// once lendingStatus below re-triggers setupLendingToolbarActions().
|
|
844
|
-
// recoveringFromLoanExpiry itself is consumed/reset there, not
|
|
845
|
-
// here — this ONLY applies to an actual recovery, not a routine
|
|
846
|
-
// renewal (re-running br.init() on every routine top-up would be
|
|
847
|
-
// needlessly disruptive). create_token's own retry logic (see
|
|
848
|
-
// LoanTokenPoller) handles the datanode write-propagation race
|
|
849
|
-
// for both recovery and routine renewals.
|
|
765
|
+
// Only now is it safe to re-initialize BookReader — not for a
|
|
766
|
+
// routine top-up, where that would be disruptive.
|
|
850
767
|
this.postInitComplete = false;
|
|
851
768
|
}
|
|
852
769
|
|
|
@@ -870,46 +787,34 @@ export default class IABookActions extends LitElement {
|
|
|
870
787
|
this.loanRenewInProgress = false;
|
|
871
788
|
}
|
|
872
789
|
|
|
873
|
-
/**
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
* @memberof IABookActions
|
|
877
|
-
*/
|
|
790
|
+
/** Start the countdown interval; ticks read the live secondsLeftOnLoan
|
|
791
|
+
* each time rather than a value frozen at start, so drift can't compound
|
|
792
|
+
* across ticks (see loanStatusCheckInterval). */
|
|
878
793
|
async startTimerCountdown() {
|
|
879
794
|
window?.IALendingIntervals?.clearTimerCountdown();
|
|
880
|
-
|
|
881
|
-
const secondsLeft = Number(this.lendingStatus.secondsLeftOnLoan);
|
|
882
795
|
this.timeWhenTimerStart = new Date();
|
|
883
796
|
|
|
884
797
|
window.IALendingIntervals.timerCountdown = setInterval(async () => {
|
|
885
|
-
|
|
886
|
-
|
|
798
|
+
await this.loanStatusCheckInterval(
|
|
799
|
+
Number(this.lendingStatus.secondsLeftOnLoan)
|
|
800
|
+
);
|
|
887
801
|
}, this.timerExecutionSeconds * 1000);
|
|
888
802
|
}
|
|
889
803
|
|
|
890
804
|
/**
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
* - loan-renew attempt to renew the loan OR show relavant message
|
|
894
|
-
* - if loan has expired, clear interval timers
|
|
895
|
-
*
|
|
805
|
+
* Runs on every countdown tick: resyncs against wall-clock time, attempts
|
|
806
|
+
* a renewal near expiry, and clears the timers once the loan expires.
|
|
896
807
|
* @param {Number} secondsLeftOnLoan
|
|
897
808
|
*/
|
|
898
809
|
async loanStatusCheckInterval(secondsLeftOnLoan) {
|
|
899
810
|
let secondsLeft = secondsLeftOnLoan;
|
|
900
811
|
secondsLeft -= this.timerExecutionSeconds;
|
|
901
|
-
secondsLeft = Math.round(secondsLeft);
|
|
812
|
+
secondsLeft = Math.round(secondsLeft);
|
|
902
813
|
|
|
903
|
-
// re-sync timer if gone off because of background window
|
|
904
|
-
// side effect: updates this.lendingStatus & kicks off lifecycle,
|
|
905
|
-
// if really updated, escape from here
|
|
906
814
|
const resyncd = this.reSyncTimerIfGoneOff(secondsLeft);
|
|
907
|
-
|
|
908
815
|
if (resyncd.hasSynced) {
|
|
909
816
|
secondsLeft = resyncd.whatShouldLeft;
|
|
910
|
-
log('[IABookActions] timer: timer re-synced', {
|
|
911
|
-
secondsLeft,
|
|
912
|
-
});
|
|
817
|
+
log('[IABookActions] timer: timer re-synced', { secondsLeft });
|
|
913
818
|
}
|
|
914
819
|
|
|
915
820
|
log('[IABookActions] timer', {
|
|
@@ -917,19 +822,20 @@ export default class IABookActions extends LitElement {
|
|
|
917
822
|
whatIsleft: secondsLeft,
|
|
918
823
|
});
|
|
919
824
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
825
|
+
// Re-anchor every tick so drift from wall-clock time can't compound
|
|
826
|
+
// across ticks that don't happen to trigger a resync above.
|
|
827
|
+
this.timeWhenTimerStart = new Date();
|
|
828
|
+
this.lendingStatus = { ...this.lendingStatus, secondsLeftOnLoan: secondsLeft };
|
|
829
|
+
|
|
830
|
+
// 10 minutes out: start checking for a renewal. 0: show the "about to
|
|
831
|
+
// auto-return" warning. @see IABookActions::bindLoanRenewEvents
|
|
926
832
|
if (secondsLeft <= this.loanRenewTimeConfig.loanRenewAtLast) {
|
|
927
833
|
await this.loanRenewAttempt(secondsLeft);
|
|
928
834
|
}
|
|
929
835
|
|
|
930
|
-
// clear interval in secondsLeft if less
|
|
931
836
|
if (secondsLeft <= this.timerExecutionSeconds) {
|
|
932
|
-
|
|
837
|
+
window?.IALendingIntervals?.clearAll();
|
|
838
|
+
this.tokenPoller?.disconnectedCallback();
|
|
933
839
|
this.sentryCaptureMsg(sentryLogs.clearOneHourTimer);
|
|
934
840
|
}
|
|
935
841
|
}
|
|
@@ -984,13 +890,8 @@ export default class IABookActions extends LitElement {
|
|
|
984
890
|
*/
|
|
985
891
|
async loanRenewAttempt(secondsLeft) {
|
|
986
892
|
let loanSecondsLeft = secondsLeft;
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
* 1. less time to execute ajax call
|
|
990
|
-
* 2. less time to write loan on datanodes
|
|
991
|
-
* 3. less time to load images by create_token api
|
|
992
|
-
* so if seconds left is < 50, just expire the loan
|
|
993
|
-
*/
|
|
893
|
+
// Under 50s left there isn't enough time for the renew_loan round-trip
|
|
894
|
+
// and create_token to load images, so just expire the loan.
|
|
994
895
|
if (loanSecondsLeft < 50) {
|
|
995
896
|
log('[IABookActions] loanRenewAttempt: < 50s left, expiring loan');
|
|
996
897
|
await this.browseHasExpired();
|
|
@@ -999,17 +900,13 @@ export default class IABookActions extends LitElement {
|
|
|
999
900
|
|
|
1000
901
|
await this.autoLoanRenewChecker(false);
|
|
1001
902
|
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
1004
|
-
// tick — only an actual renewal (see handleLoanAutoRenewed) re-arms it.
|
|
903
|
+
// Once dismissed, don't re-show the warning on every subsequent tick —
|
|
904
|
+
// only an actual renewal (handleLoanAutoRenewed) re-arms it.
|
|
1005
905
|
if (
|
|
1006
906
|
this.loanRenewResult.renewNow === false &&
|
|
1007
907
|
!this.warningModalDismissed
|
|
1008
908
|
) {
|
|
1009
|
-
|
|
1010
|
-
* so compensate for the 50 second buffer to handle above race conditions
|
|
1011
|
-
* let's reduce 1 min from warning texts and early return the book when 1 min left.
|
|
1012
|
-
*/
|
|
909
|
+
// Compensate for the 50s buffer above by warning a minute early.
|
|
1013
910
|
loanSecondsLeft -= 60;
|
|
1014
911
|
this.loanRenewResult.secondsLeft = loanSecondsLeft;
|
|
1015
912
|
|
|
@@ -1022,27 +919,49 @@ export default class IABookActions extends LitElement {
|
|
|
1022
919
|
* @see LoanTokenPoller
|
|
1023
920
|
*/
|
|
1024
921
|
startLoanTokenPoller() {
|
|
1025
|
-
const successCallback = () => {
|
|
922
|
+
const successCallback = async () => {
|
|
1026
923
|
if (!this.postInitComplete) {
|
|
1027
924
|
this.lendingBarPostInit();
|
|
1028
925
|
}
|
|
1029
926
|
this.postInitComplete = true;
|
|
927
|
+
|
|
928
|
+
if (this.awaitingTokenRecovery) {
|
|
929
|
+
log(
|
|
930
|
+
'[IABookActions] create_token recovered — closing error modal, resuming timer',
|
|
931
|
+
{ identifier: this.identifier }
|
|
932
|
+
);
|
|
933
|
+
this.awaitingTokenRecovery = false;
|
|
934
|
+
this.tokenRecoveryAttempts = 0;
|
|
935
|
+
this.modal?.closeModal();
|
|
936
|
+
this.modal.removeAttribute('id');
|
|
937
|
+
this.modal.customModalContent = nothing;
|
|
938
|
+
|
|
939
|
+
// Recompute the true remaining time from the cache — not however
|
|
940
|
+
// long the outage lasted — before resuming the stopped timers.
|
|
941
|
+
const loanTime = await this.localCache.get(
|
|
942
|
+
`${this.identifier}-loanTime`
|
|
943
|
+
);
|
|
944
|
+
const rawSecondsLeft = Math.round((loanTime - new Date()) / 1000);
|
|
945
|
+
const secondsLeft =
|
|
946
|
+
Number.isFinite(rawSecondsLeft) && rawSecondsLeft > 0
|
|
947
|
+
? rawSecondsLeft
|
|
948
|
+
: this.loanRenewTimeConfig.loanTotalTime;
|
|
949
|
+
this.lendingStatus = {
|
|
950
|
+
...this.lendingStatus,
|
|
951
|
+
secondsLeftOnLoan: secondsLeft,
|
|
952
|
+
};
|
|
953
|
+
await this.startTimerCountdown();
|
|
954
|
+
await this.startBrowseTimer();
|
|
955
|
+
}
|
|
1030
956
|
};
|
|
1031
957
|
const errorCallback = eventObj => {
|
|
1032
958
|
this.handleLendingActionError(eventObj);
|
|
1033
959
|
};
|
|
1034
960
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
* 2. this.borrowType
|
|
1040
|
-
* 3. successCallback - it used to
|
|
1041
|
-
* - disptach lendingFlow::PostInit event
|
|
1042
|
-
* - initialize bookreader using br.init()
|
|
1043
|
-
* 4. errorCallback
|
|
1044
|
-
* 5. tokenPollerDelay
|
|
1045
|
-
*/
|
|
961
|
+
// Tear down any previous poller first — otherwise its own pending
|
|
962
|
+
// internal retry timeout (see LoanTokenPoller.handleTokenError) is
|
|
963
|
+
// never cancelled and can fire later against stale state.
|
|
964
|
+
this.tokenPoller?.disconnectedCallback();
|
|
1046
965
|
this.tokenPoller = new LoanTokenPoller(
|
|
1047
966
|
this.identifier,
|
|
1048
967
|
this.borrowType,
|
|
@@ -1062,13 +981,12 @@ export default class IABookActions extends LitElement {
|
|
|
1062
981
|
}
|
|
1063
982
|
|
|
1064
983
|
/**
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
984
|
+
* Handles lending errors from any action (browse_book, borrow_book,
|
|
985
|
+
* create_token, renew_loan, etc).
|
|
1068
986
|
* @event IABookActions#lendingActionError
|
|
1069
|
-
* @param {Object} event
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
987
|
+
* @param {Object} event
|
|
988
|
+
* @param {string} event.detail.action
|
|
989
|
+
* @param {string} event.detail.data.error
|
|
1072
990
|
*/
|
|
1073
991
|
handleLendingActionError(event) {
|
|
1074
992
|
this.disableActionGroup = false;
|
|
@@ -1087,23 +1005,12 @@ export default class IABookActions extends LitElement {
|
|
|
1087
1005
|
});
|
|
1088
1006
|
|
|
1089
1007
|
if (action === 'create_token') {
|
|
1090
|
-
// A create_token hiccup
|
|
1091
|
-
//
|
|
1092
|
-
// itself, so it must not stop the reading countdown. Only clear the
|
|
1093
|
-
// token poller so it can be retried.
|
|
1008
|
+
// A create_token hiccup says nothing about how much time is left on
|
|
1009
|
+
// the loan, so it must not stop the countdown by itself.
|
|
1094
1010
|
window?.IALendingIntervals?.clearTokenPoller();
|
|
1095
1011
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
* can't be initialized at all, so the book genuinely won't open and
|
|
1099
|
-
* they need to know. A routine interval refresh failing is a
|
|
1100
|
-
* different thing — the patron is already reading, the pages they
|
|
1101
|
-
* have are still served, and LoanTokenPoller will try again on the
|
|
1102
|
-
* next tick. Interrupting a mid-read session with a blocking modal
|
|
1103
|
-
* over a transient blip (the recurring 405s QA is seeing, say) is
|
|
1104
|
-
* worse than the blip. This is why the original code suppressed
|
|
1105
|
-
* create_token errors outright; keep that for the interval case.
|
|
1106
|
-
*/
|
|
1012
|
+
// Only the INITIAL token matters enough to interrupt the patron —
|
|
1013
|
+
// a routine interval refresh stays silent and just retries next tick.
|
|
1107
1014
|
if (!isInitial) {
|
|
1108
1015
|
log(
|
|
1109
1016
|
'[IABookActions] create_token failed on interval refresh — staying silent, poller will retry'
|
|
@@ -1111,18 +1018,26 @@ export default class IABookActions extends LitElement {
|
|
|
1111
1018
|
return;
|
|
1112
1019
|
}
|
|
1113
1020
|
|
|
1114
|
-
// The loan itself is
|
|
1115
|
-
//
|
|
1116
|
-
//
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1021
|
+
// The loan itself is still active (do_renew succeeded) — don't touch
|
|
1022
|
+
// lendingStatus, which would collapse borrowType to null and strand
|
|
1023
|
+
// the poller from ever restarting. Still nothing to read right now
|
|
1024
|
+
// though, so stop the countdown timers too; startLoanTokenPoller()'s
|
|
1025
|
+
// successCallback resumes them once access recovers.
|
|
1026
|
+
this.awaitingTokenRecovery = true;
|
|
1027
|
+
window?.IALendingIntervals?.clearTimerCountdown();
|
|
1028
|
+
window?.IALendingIntervals?.clearBrowseExpireTimeout();
|
|
1029
|
+
|
|
1030
|
+
// Bounded: a genuinely-lost (not just slow-to-propagate) loan would
|
|
1031
|
+
// otherwise retry this forever.
|
|
1032
|
+
this.tokenRecoveryAttempts += 1;
|
|
1033
|
+
if (this.tokenRecoveryAttempts <= this.maxTokenRecoveryAttempts) {
|
|
1034
|
+
this.startLoanTokenPoller();
|
|
1035
|
+
} else {
|
|
1036
|
+
log('[IABookActions] create_token recovery attempts exhausted', {
|
|
1037
|
+
identifier: this.identifier,
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1122
1040
|
|
|
1123
|
-
// LoanTokenPoller has already exhausted its retries for the
|
|
1124
|
-
// transient datanode-propagation race by the time we get here (or
|
|
1125
|
-
// the error was never that), so this is a genuine failure.
|
|
1126
1041
|
// showErrorModal has dedicated create_token messaging (refresh
|
|
1127
1042
|
// button + support email).
|
|
1128
1043
|
if (errorMsg) this.showErrorModal(errorMsg, action);
|
|
@@ -1131,11 +1046,8 @@ export default class IABookActions extends LitElement {
|
|
|
1131
1046
|
this.loanRenewInProgress = false;
|
|
1132
1047
|
this.recoveringFromLoanExpiry = false;
|
|
1133
1048
|
|
|
1134
|
-
// The loan was NOT
|
|
1135
|
-
//
|
|
1136
|
-
// the action bar showing "Return now" with a frozen countdown from
|
|
1137
|
-
// before the failed attempt, which falsely implies the book is
|
|
1138
|
-
// still actively being read.
|
|
1049
|
+
// The loan was NOT renewed — reflect that immediately (show Borrow,
|
|
1050
|
+
// clear the stale timer) rather than leaving a frozen "Return now".
|
|
1139
1051
|
this.lendingStatus = {
|
|
1140
1052
|
...this.lendingStatus,
|
|
1141
1053
|
user_has_browsed: false,
|
|
@@ -1143,11 +1055,8 @@ export default class IABookActions extends LitElement {
|
|
|
1143
1055
|
secondsLeftOnLoan: 0,
|
|
1144
1056
|
};
|
|
1145
1057
|
|
|
1146
|
-
//
|
|
1147
|
-
//
|
|
1148
|
-
// now authoritatively considers true — matching on "not available"
|
|
1149
|
-
// to guess the reason was fragile since the server can return any
|
|
1150
|
-
// message (e.g. a lending limit).
|
|
1058
|
+
// Refresh the page on dismissal so the client picks up whatever the
|
|
1059
|
+
// server now authoritatively considers true (any error message).
|
|
1151
1060
|
this.showLoanUnavailableModal(errorMsg);
|
|
1152
1061
|
} else {
|
|
1153
1062
|
// Every other action failure genuinely affects loan state.
|
|
@@ -755,6 +755,35 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
755
755
|
expect(el.loanRenewResult.renewNow).to.be.true;
|
|
756
756
|
});
|
|
757
757
|
|
|
758
|
+
it('does not start a new renewal while awaiting create_token recovery from the last one', async () => {
|
|
759
|
+
// loanRenewInProgress clears as soon as renew_loan itself succeeds
|
|
760
|
+
// (see handleLoanAutoRenewed), before create_token is known to work.
|
|
761
|
+
// A visibilitychange/userAction landing in that window must not kick
|
|
762
|
+
// off ANOTHER renewal — that repeats the do_renew() delete+create
|
|
763
|
+
// write for no reason. See WEBDEV-8322 production log, 2026-09-28.
|
|
764
|
+
const el = await fixture(
|
|
765
|
+
container({
|
|
766
|
+
userid: '@user1',
|
|
767
|
+
identifier: 'foobar',
|
|
768
|
+
lendingStatus: {
|
|
769
|
+
user_has_browsed: true,
|
|
770
|
+
browsingExpired: true,
|
|
771
|
+
},
|
|
772
|
+
})
|
|
773
|
+
);
|
|
774
|
+
await el.updateComplete;
|
|
775
|
+
|
|
776
|
+
el.loanRenewInProgress = false;
|
|
777
|
+
el.awaitingTokenRecovery = true;
|
|
778
|
+
|
|
779
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
780
|
+
el.autoRenewExpiredLoan();
|
|
781
|
+
|
|
782
|
+
expect(spy.callCount).to.equal(1);
|
|
783
|
+
expect(el.loanRenewInProgress).to.be.false;
|
|
784
|
+
expect(el.loanRenewResult.renewNow).to.be.false;
|
|
785
|
+
});
|
|
786
|
+
|
|
758
787
|
it('clears loanRenewInProgress after a successful loanAutoRenewed event', async () => {
|
|
759
788
|
const el = await fixture(
|
|
760
789
|
container({
|
|
@@ -1135,7 +1164,49 @@ describe('handleLendingActionError - create_token failures must not stop the cou
|
|
|
1135
1164
|
|
|
1136
1165
|
expect(showErrorModalSpy.calledOnceWith(errorMsg, 'create_token')).to.be
|
|
1137
1166
|
.true;
|
|
1138
|
-
|
|
1167
|
+
// The loan itself is still active (do_renew succeeded) — flipping this
|
|
1168
|
+
// to false used to collapse borrowType to null in
|
|
1169
|
+
// getCurrentLendingActions(), which made setupLendingToolbarActions()
|
|
1170
|
+
// bail out before ever restarting the token poller, silently
|
|
1171
|
+
// stranding the patron until the countdown force-returned the book.
|
|
1172
|
+
// See WEBDEV-8322 production log, 2026-09-28.
|
|
1173
|
+
expect(el.lendingStatus.user_has_browsed).to.be.true;
|
|
1174
|
+
expect(el.awaitingTokenRecovery).to.be.true;
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
it('stops the countdown/expiry timers on a failed INITIAL create_token — nothing to read yet', async () => {
|
|
1178
|
+
// Unlike the interval-refresh case above (pages already loaded, keep
|
|
1179
|
+
// ticking), an initial create_token failure means BookReader never
|
|
1180
|
+
// got page images — there's nothing to read. Letting the countdown
|
|
1181
|
+
// keep running just re-syncs on every single tick against a broken
|
|
1182
|
+
// session (see WEBDEV-8322 QA log, 2026-09-28) until the loan
|
|
1183
|
+
// eventually force-expires. Stop both timers instead; they resume
|
|
1184
|
+
// once startLoanTokenPoller()'s successCallback confirms recovery.
|
|
1185
|
+
const el = await fixture(
|
|
1186
|
+
container({
|
|
1187
|
+
userid: '@user1',
|
|
1188
|
+
identifier: 'foobar',
|
|
1189
|
+
lendingStatus: {
|
|
1190
|
+
user_has_browsed: true,
|
|
1191
|
+
browsingExpired: false,
|
|
1192
|
+
secondsLeftOnLoan: 100,
|
|
1193
|
+
},
|
|
1194
|
+
})
|
|
1195
|
+
);
|
|
1196
|
+
await el.updateComplete;
|
|
1197
|
+
expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
|
|
1198
|
+
expect(window.IALendingIntervals.browseExpireTimeout).to.not.equal(0);
|
|
1199
|
+
|
|
1200
|
+
el.handleLendingActionError({
|
|
1201
|
+
detail: {
|
|
1202
|
+
action: 'create_token',
|
|
1203
|
+
isInitial: true,
|
|
1204
|
+
data: { error: 'loan token not found. please try again later.' },
|
|
1205
|
+
},
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
expect(window.IALendingIntervals.timerCountdown).to.equal(0);
|
|
1209
|
+
expect(window.IALendingIntervals.browseExpireTimeout).to.equal(0);
|
|
1139
1210
|
});
|
|
1140
1211
|
|
|
1141
1212
|
it('still clears everything on a renew_loan failure', async () => {
|
|
@@ -1538,6 +1609,104 @@ describe('WEBDEV-8322 review fixes', () => {
|
|
|
1538
1609
|
await el.updateComplete;
|
|
1539
1610
|
|
|
1540
1611
|
expect(spy.calledOnce).to.be.true;
|
|
1541
|
-
|
|
1612
|
+
// Still reading — see the identical assertion above for why this must
|
|
1613
|
+
// stay true (WEBDEV-8322 production log, 2026-09-28).
|
|
1614
|
+
expect(el.lendingStatus.user_has_browsed).to.be.true;
|
|
1615
|
+
expect(el.awaitingTokenRecovery).to.be.true;
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
it('does not resume the countdown via visibilitychange while awaiting create_token recovery', async () => {
|
|
1619
|
+
// Regression: visibilitychange's resync path didn't check
|
|
1620
|
+
// awaitingTokenRecovery, so backgrounding/foregrounding the tab during
|
|
1621
|
+
// a create_token outage could resume the timers that
|
|
1622
|
+
// handleLendingActionError had deliberately stopped, and auto-return a
|
|
1623
|
+
// loan the server still considers valid.
|
|
1624
|
+
const el = await fixture(
|
|
1625
|
+
container({
|
|
1626
|
+
userid: '@user1',
|
|
1627
|
+
identifier: 'foobar',
|
|
1628
|
+
lendingStatus: {
|
|
1629
|
+
user_has_browsed: true,
|
|
1630
|
+
browsingExpired: false,
|
|
1631
|
+
secondsLeftOnLoan: 300,
|
|
1632
|
+
},
|
|
1633
|
+
})
|
|
1634
|
+
);
|
|
1635
|
+
await el.updateComplete;
|
|
1636
|
+
el.awaitingTokenRecovery = true;
|
|
1637
|
+
|
|
1638
|
+
const spy = Sinon.spy(el, 'loanStatusCheckInterval');
|
|
1639
|
+
Object.defineProperty(document, 'hidden', {
|
|
1640
|
+
value: false,
|
|
1641
|
+
configurable: true,
|
|
1642
|
+
});
|
|
1643
|
+
document.dispatchEvent(new Event('visibilitychange'));
|
|
1644
|
+
await aTimeout(100);
|
|
1645
|
+
|
|
1646
|
+
expect(spy.called).to.be.false;
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
it('tears down the previous poller before starting a new one', async () => {
|
|
1650
|
+
// Regression: startLoanTokenPoller() always replaced this.tokenPoller
|
|
1651
|
+
// without cancelling the old instance's pending internal retry timer,
|
|
1652
|
+
// which could later fire against stale state.
|
|
1653
|
+
const el = await fixture(
|
|
1654
|
+
container({
|
|
1655
|
+
userid: '@user1',
|
|
1656
|
+
identifier: 'poller-teardown',
|
|
1657
|
+
lendingStatus: {
|
|
1658
|
+
is_lendable: true,
|
|
1659
|
+
available_to_browse: true,
|
|
1660
|
+
user_has_browsed: true,
|
|
1661
|
+
browsingExpired: false,
|
|
1662
|
+
secondsLeftOnLoan: 300,
|
|
1663
|
+
},
|
|
1664
|
+
})
|
|
1665
|
+
);
|
|
1666
|
+
await el.updateComplete;
|
|
1667
|
+
|
|
1668
|
+
el.startLoanTokenPoller();
|
|
1669
|
+
const firstPoller = el.tokenPoller;
|
|
1670
|
+
const disconnectSpy = Sinon.spy(firstPoller, 'disconnectedCallback');
|
|
1671
|
+
|
|
1672
|
+
el.startLoanTokenPoller();
|
|
1673
|
+
|
|
1674
|
+
expect(disconnectSpy.calledOnce).to.be.true;
|
|
1675
|
+
expect(el.tokenPoller).to.not.equal(firstPoller);
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
it('stops restarting the poller after maxTokenRecoveryAttempts consecutive create_token failures', async () => {
|
|
1679
|
+
// Regression: an outer restart loop with no cap meant a genuinely-lost
|
|
1680
|
+
// (not just slow-to-propagate) loan retried forever.
|
|
1681
|
+
const el = await fixture(
|
|
1682
|
+
container({
|
|
1683
|
+
userid: '@user1',
|
|
1684
|
+
identifier: 'token-exhausted',
|
|
1685
|
+
lendingStatus: {
|
|
1686
|
+
is_lendable: true,
|
|
1687
|
+
available_to_browse: true,
|
|
1688
|
+
user_has_browsed: true,
|
|
1689
|
+
browsingExpired: false,
|
|
1690
|
+
secondsLeftOnLoan: 300,
|
|
1691
|
+
},
|
|
1692
|
+
})
|
|
1693
|
+
);
|
|
1694
|
+
await el.updateComplete;
|
|
1695
|
+
|
|
1696
|
+
const startSpy = Sinon.spy(el, 'startLoanTokenPoller');
|
|
1697
|
+
const errorMsg = 'loan token not found. please try again later.';
|
|
1698
|
+
|
|
1699
|
+
for (let i = 0; i < el.maxTokenRecoveryAttempts + 1; i += 1) {
|
|
1700
|
+
el.handleLendingActionError({
|
|
1701
|
+
detail: {
|
|
1702
|
+
action: 'create_token',
|
|
1703
|
+
isInitial: true,
|
|
1704
|
+
data: { error: errorMsg },
|
|
1705
|
+
},
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
expect(startSpy.callCount).to.equal(el.maxTokenRecoveryAttempts);
|
|
1710
|
+
expect(el.tokenRecoveryAttempts).to.equal(el.maxTokenRecoveryAttempts + 1);
|
|
1542
1711
|
});
|
|
1543
1712
|
});
|