@internetarchive/ia-book-actions 1.4.4-alpha13 → 1.4.4-alpha15
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/index.html +38 -1
- package/package.json +1 -1
- package/src/core/services/actions-handler/actions-handler-service.js +12 -4
- package/src/core/services/loan-token-poller.js +5 -65
- package/src/ia-book-actions.js +98 -250
- package/test/core/services/loan-token-poller.test.js +12 -101
- package/test/ia-book-actions.test.js +73 -14
package/index.html
CHANGED
|
@@ -360,7 +360,33 @@
|
|
|
360
360
|
});
|
|
361
361
|
});
|
|
362
362
|
|
|
363
|
-
|
|
363
|
+
// WEBDEV-8322 repro: fail only create_token (via ?failAction=create_token),
|
|
364
|
+
// leaving renew_loan (and every other action) succeeding normally —
|
|
365
|
+
// see actions-handler-service.js's shouldReturnError.
|
|
366
|
+
document.querySelectorAll('.failCreateToken input[type=checkbox]').forEach((item) => {
|
|
367
|
+
if (window.location.href.indexOf('?failAction=create_token') !== -1) {
|
|
368
|
+
item.checked = true;
|
|
369
|
+
}
|
|
370
|
+
item.addEventListener('click', (e) => {
|
|
371
|
+
console.log(`[Demo] fail-create_token-only mode ${e.target.checked ? 'enabled' : 'disabled'}`);
|
|
372
|
+
|
|
373
|
+
var searchParams = new URLSearchParams(window.location.search);
|
|
374
|
+
var separate = '';
|
|
375
|
+
if (e.target.checked) {
|
|
376
|
+
separate = '?';
|
|
377
|
+
searchParams.set('failAction', 'create_token');
|
|
378
|
+
} else {
|
|
379
|
+
separate = '';
|
|
380
|
+
searchParams.delete('failAction');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (history.pushState) {
|
|
384
|
+
var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + separate + searchParams;
|
|
385
|
+
window.history.pushState({path:newurl},'',newurl);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
364
390
|
document.querySelectorAll('.userState input[type=checkbox]').forEach((item) => {
|
|
365
391
|
item.addEventListener('click', (e) => {
|
|
366
392
|
if (e.target.checked) {
|
|
@@ -726,6 +752,17 @@
|
|
|
726
752
|
</span>
|
|
727
753
|
</td>
|
|
728
754
|
</tr>
|
|
755
|
+
<tr colspan='5'>
|
|
756
|
+
<td class="failCreateToken">
|
|
757
|
+
<label><input type="checkbox" name="failCreateToken" value="failCreateToken" />Fail create_token only (renew_loan still succeeds)</label>
|
|
758
|
+
<span style="color: #666; font-size: 11px; display: block; padding: 0 10px 10px;">
|
|
759
|
+
WEBDEV-8322 repro: unlike the checkbox above, this leaves renew_loan
|
|
760
|
+
succeeding and fails only the create_token call right after it — turn
|
|
761
|
+
this on, then run scenario 2 → 3 (or 2 → 4) below. Expect the error
|
|
762
|
+
modal, and the bar/timer resetting to Borrow/0 (no retry).
|
|
763
|
+
</span>
|
|
764
|
+
</td>
|
|
765
|
+
</tr>
|
|
729
766
|
<tr>
|
|
730
767
|
<td class="userState">
|
|
731
768
|
<label><input type="checkbox" name="user" value="isAdmin" />isAdmin</label>
|
package/package.json
CHANGED
|
@@ -18,7 +18,6 @@ export default async function ActionsHandlerService(options) {
|
|
|
18
18
|
let baseHost = '/services/loans/loan';
|
|
19
19
|
const location = window?.location;
|
|
20
20
|
|
|
21
|
-
// return error reponse when not production and has ?error=true param...
|
|
22
21
|
const tokenError = 'loan token not found. please try again later.';
|
|
23
22
|
const borrowError =
|
|
24
23
|
'This book is not available to borrow at this time. Please try again later.';
|
|
@@ -29,9 +28,18 @@ export default async function ActionsHandlerService(options) {
|
|
|
29
28
|
'renew_loan',
|
|
30
29
|
'return_loan',
|
|
31
30
|
];
|
|
31
|
+
const searchParams = new URLSearchParams(location?.search);
|
|
32
|
+
// ?error=true fails every erroneous action (e.g. simulates the book
|
|
33
|
+
// already being borrowed by someone else). ?failAction=create_token (or
|
|
34
|
+
// any action name) fails ONLY that action, so a renew_loan can succeed
|
|
35
|
+
// and the immediately-following create_token can be made to fail —
|
|
36
|
+
// reproducing WEBDEV-8322's "renewed, but create_token failed" bug
|
|
37
|
+
// without needing a real book.
|
|
32
38
|
const shouldReturnError =
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
location?.hostname !== 'archive.org' &&
|
|
40
|
+
erroneousActions.includes(option?.action) &&
|
|
41
|
+
(searchParams.get('error') === 'true' ||
|
|
42
|
+
searchParams.get('failAction') === option?.action);
|
|
35
43
|
|
|
36
44
|
const testHostname = ['localhost', 'internetarchive.github.io'];
|
|
37
45
|
let isTest = false;
|
|
@@ -51,7 +59,7 @@ export default async function ActionsHandlerService(options) {
|
|
|
51
59
|
})
|
|
52
60
|
.then(async response => {
|
|
53
61
|
// intentional error on localhost
|
|
54
|
-
if (shouldReturnError
|
|
62
|
+
if (shouldReturnError) {
|
|
55
63
|
return {
|
|
56
64
|
success: false,
|
|
57
65
|
error: option?.action === 'create_token' ? tokenError : borrowError,
|
|
@@ -19,26 +19,6 @@ export class LoanTokenPoller {
|
|
|
19
19
|
|
|
20
20
|
this.loanTokenInterval = undefined;
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Lending::do_renew() (backend) deletes the old loan record and
|
|
24
|
-
* writes a brand-new one on every renewal — this poller's initial
|
|
25
|
-
* create_token call (fired right after a renewal re-establishes the
|
|
26
|
-
* reading session) can race that write's propagation and come back
|
|
27
|
-
* with "you do not currently have this book borrowed" even though
|
|
28
|
-
* the loan is genuinely valid. Retry a few times with backoff instead
|
|
29
|
-
* of giving up on the first attempt.
|
|
30
|
-
*/
|
|
31
|
-
this.maxTokenRetries = 3;
|
|
32
|
-
this.tokenRetryDelay = 1000; // ms, multiplied by attempt number
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Handle for a pending retry, so teardown can cancel it. It's a bare
|
|
36
|
-
* setTimeout rather than an IALendingIntervals entry, so without this
|
|
37
|
-
* a retry scheduled just before the loan is returned would still fire
|
|
38
|
-
* its create_token afterwards.
|
|
39
|
-
*/
|
|
40
|
-
this.retryTimeout = undefined;
|
|
41
|
-
|
|
42
22
|
/**
|
|
43
23
|
* loan analytics instance
|
|
44
24
|
* @see loan-analytics.js
|
|
@@ -50,8 +30,6 @@ export class LoanTokenPoller {
|
|
|
50
30
|
|
|
51
31
|
disconnectedCallback() {
|
|
52
32
|
window?.IALendingIntervals?.clearTokenPoller();
|
|
53
|
-
clearTimeout(this.retryTimeout);
|
|
54
|
-
this.retryTimeout = undefined;
|
|
55
33
|
}
|
|
56
34
|
|
|
57
35
|
async bookAccessed() {
|
|
@@ -84,24 +62,21 @@ export class LoanTokenPoller {
|
|
|
84
62
|
/**
|
|
85
63
|
* @param {boolean} isInitial - the first create_token call right after
|
|
86
64
|
* bookAccessed()/a renewal, as opposed to a routine interval tick.
|
|
87
|
-
* @param {number} retryCount - internal, counts retries of the initial call.
|
|
88
65
|
*/
|
|
89
|
-
async handleLoanTokenPoller(isInitial = false
|
|
66
|
+
async handleLoanTokenPoller(isInitial = false) {
|
|
90
67
|
const action = 'create_token';
|
|
91
68
|
log('[LoanTokenPoller] create_token requested', {
|
|
92
69
|
identifier: this.identifier,
|
|
93
70
|
isInitial,
|
|
94
|
-
retryCount,
|
|
95
71
|
});
|
|
96
72
|
ActionsHandlerService({
|
|
97
73
|
identifier: this.identifier,
|
|
98
74
|
action,
|
|
99
|
-
error: data => this.handleTokenError(data, isInitial
|
|
75
|
+
error: data => this.handleTokenError(data, isInitial),
|
|
100
76
|
success: () => {
|
|
101
77
|
log('[LoanTokenPoller] create_token succeeded', {
|
|
102
78
|
identifier: this.identifier,
|
|
103
79
|
isInitial,
|
|
104
|
-
retryCount,
|
|
105
80
|
});
|
|
106
81
|
if (isInitial) this.successCallback();
|
|
107
82
|
},
|
|
@@ -109,58 +84,23 @@ export class LoanTokenPoller {
|
|
|
109
84
|
}
|
|
110
85
|
|
|
111
86
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* comment) or reported via errorCallback. Split out from
|
|
87
|
+
* Reports a create_token failure via errorCallback — no retry, it's
|
|
88
|
+
* treated as terminal on the first failure. Split out from
|
|
115
89
|
* handleLoanTokenPoller so it's directly testable without needing to
|
|
116
90
|
* mock the network call.
|
|
117
91
|
*
|
|
118
92
|
* @param {Object} data - the error payload from ActionsHandlerService.
|
|
119
93
|
* @param {boolean} isInitial
|
|
120
|
-
* @param {number} retryCount
|
|
121
94
|
*/
|
|
122
|
-
handleTokenError(data, isInitial
|
|
95
|
+
handleTokenError(data, isInitial) {
|
|
123
96
|
const action = 'create_token';
|
|
124
|
-
const isStaleLoanReadError =
|
|
125
|
-
typeof data?.error === 'string' &&
|
|
126
|
-
/do not currently have this book borrowed/i.test(data.error);
|
|
127
97
|
|
|
128
98
|
log('[LoanTokenPoller] create_token failed', {
|
|
129
99
|
identifier: this.identifier,
|
|
130
100
|
isInitial,
|
|
131
|
-
retryCount,
|
|
132
|
-
isStaleLoanReadError,
|
|
133
101
|
error: data?.error,
|
|
134
102
|
});
|
|
135
103
|
|
|
136
|
-
// The renewal write can be mid-propagation for an interval refresh too,
|
|
137
|
-
// not just the initial call — a routine tick landing moments after a
|
|
138
|
-
// renewal hits the same race. Retry both; isInitial only decides how a
|
|
139
|
-
// final failure is reported, not whether it's worth retrying.
|
|
140
|
-
if (isStaleLoanReadError && retryCount < this.maxTokenRetries) {
|
|
141
|
-
const delay = this.tokenRetryDelay * (retryCount + 1);
|
|
142
|
-
log(
|
|
143
|
-
'[LoanTokenPoller] retrying create_token after stale-loan-read error',
|
|
144
|
-
{
|
|
145
|
-
identifier: this.identifier,
|
|
146
|
-
nextRetryCount: retryCount + 1,
|
|
147
|
-
delay,
|
|
148
|
-
}
|
|
149
|
-
);
|
|
150
|
-
clearTimeout(this.retryTimeout);
|
|
151
|
-
this.retryTimeout = setTimeout(() => {
|
|
152
|
-
this.retryTimeout = undefined;
|
|
153
|
-
this.handleLoanTokenPoller(isInitial, retryCount + 1);
|
|
154
|
-
}, delay);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
log('[LoanTokenPoller] giving up on create_token, reporting error', {
|
|
159
|
-
identifier: this.identifier,
|
|
160
|
-
isInitial,
|
|
161
|
-
retryCount,
|
|
162
|
-
});
|
|
163
|
-
|
|
164
104
|
// isInitial rides along so the consumer can distinguish "the book won't
|
|
165
105
|
// open at all" from "a mid-read refresh blipped" — see
|
|
166
106
|
// IABookActions.handleLendingActionError.
|
package/src/ia-book-actions.js
CHANGED
|
@@ -97,24 +97,17 @@ export default class IABookActions extends LitElement {
|
|
|
97
97
|
this.loanRenewInProgress = false;
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* True while
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* re-initialized once the renewal is confirmed — see
|
|
100
|
+
* True while recovering from a loan that genuinely lapsed (set by
|
|
101
|
+
* autoRenewExpiredLoan), as opposed to a routine pre-expiry top-up.
|
|
102
|
+
* Only the recovery case needs BookReader re-initialized — see
|
|
104
103
|
* handleLoanAutoRenewed.
|
|
105
104
|
*/
|
|
106
105
|
this.recoveringFromLoanExpiry = false;
|
|
107
106
|
|
|
108
107
|
this.warningModalOpen = false;
|
|
109
108
|
|
|
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
|
-
*/
|
|
109
|
+
/** Once dismissed, don't re-show the warning modal every tick — only
|
|
110
|
+
* a real renewal (handleLoanAutoRenewed) re-arms it. */
|
|
118
111
|
this.warningModalDismissed = false;
|
|
119
112
|
|
|
120
113
|
/**
|
|
@@ -148,9 +141,9 @@ export default class IABookActions extends LitElement {
|
|
|
148
141
|
}
|
|
149
142
|
|
|
150
143
|
disconnectedCallback() {
|
|
151
|
-
|
|
144
|
+
super.disconnectedCallback();
|
|
152
145
|
window?.IALendingIntervals?.clearAll();
|
|
153
|
-
|
|
146
|
+
this.tokenPoller?.disconnectedCallback();
|
|
154
147
|
this.sentryCaptureMsg(sentryLogs.disconnectedCallback);
|
|
155
148
|
this.disconnectResizeObserver();
|
|
156
149
|
}
|
|
@@ -189,12 +182,8 @@ export default class IABookActions extends LitElement {
|
|
|
189
182
|
}
|
|
190
183
|
|
|
191
184
|
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.
|
|
185
|
+
// Pause the countdown for the renew_loan round-trip, for every
|
|
186
|
+
// renewal path — cleared by handleLoanAutoRenewed().
|
|
198
187
|
this.loanRenewInProgress = true;
|
|
199
188
|
window.IALendingIntervals.clearAll();
|
|
200
189
|
}
|
|
@@ -269,13 +258,8 @@ export default class IABookActions extends LitElement {
|
|
|
269
258
|
'browsingExpired' in this.lendingStatus &&
|
|
270
259
|
this.lendingStatus?.browsingExpired;
|
|
271
260
|
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.
|
|
261
|
+
// Auto-return must not visibly change the bar — unless nothing's
|
|
262
|
+
// rendered yet (fresh load of an already-expired loan).
|
|
279
263
|
if (this.primaryActions?.length) {
|
|
280
264
|
log('[IABookActions] browsing expired — leaving action bar untouched');
|
|
281
265
|
} else {
|
|
@@ -305,18 +289,18 @@ export default class IABookActions extends LitElement {
|
|
|
305
289
|
|
|
306
290
|
if (!this.applyLendingActions()) return;
|
|
307
291
|
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
|
|
292
|
+
// primaryActions/borrowType/etc are plain fields, not reactive
|
|
293
|
+
// properties, so nothing pushes them to the rendered template (and
|
|
294
|
+
// from there to <collapsible-action-group>) on its own. Do that now,
|
|
295
|
+
// unconditionally — the early returns below (not borrowed, title bar)
|
|
296
|
+
// must not skip it, or the action bar visibly goes stale (e.g. a
|
|
297
|
+
// create_token failure resetting to Borrow never actually shows it).
|
|
298
|
+
this.requestUpdate();
|
|
299
|
+
|
|
300
|
+
// Don't (re)start the countdown mid-renewal — secondsLeftOnLoan could
|
|
301
|
+
// still be stale until handleLoanAutoRenewed() confirms it.
|
|
315
302
|
if (this.borrowType === 'browsed' && !this.loanRenewInProgress) {
|
|
316
|
-
// start timer for loan-renew
|
|
317
303
|
await this.startTimerCountdown();
|
|
318
|
-
|
|
319
|
-
// start timer for browsed.
|
|
320
304
|
await this.startBrowseTimer();
|
|
321
305
|
}
|
|
322
306
|
|
|
@@ -326,15 +310,8 @@ export default class IABookActions extends LitElement {
|
|
|
326
310
|
return;
|
|
327
311
|
}
|
|
328
312
|
|
|
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
|
-
*/
|
|
313
|
+
// Wait until any in-flight renewal is confirmed before (re)starting the
|
|
314
|
+
// poller, so create_token isn't called against a not-yet-renewed loan.
|
|
338
315
|
setTimeout(() => {
|
|
339
316
|
if (
|
|
340
317
|
!hasExpired &&
|
|
@@ -345,29 +322,12 @@ export default class IABookActions extends LitElement {
|
|
|
345
322
|
this.startLoanTokenPoller();
|
|
346
323
|
}
|
|
347
324
|
}, 100);
|
|
348
|
-
|
|
349
|
-
this.requestUpdate();
|
|
350
325
|
}
|
|
351
326
|
|
|
352
327
|
/**
|
|
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
|
-
*
|
|
328
|
+
* Is this BookReader's own init-time jump to the last-read page, rather
|
|
329
|
+
* than a real patron interaction (which fires the identical event)? Only
|
|
330
|
+
* an explicit `false` counts; a missing flag must be treated as real.
|
|
371
331
|
* @param {Event} event - the BookReader:userAction event
|
|
372
332
|
* @returns {boolean}
|
|
373
333
|
*/
|
|
@@ -393,14 +353,9 @@ export default class IABookActions extends LitElement {
|
|
|
393
353
|
return;
|
|
394
354
|
}
|
|
395
355
|
|
|
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.
|
|
356
|
+
// Capture before autoRenewExpiredLoan() runs — it optimistically
|
|
357
|
+
// flips browsingExpired to false, which would otherwise also let
|
|
358
|
+
// autoLoanRenewChecker() run and clobber the in-flight renewal.
|
|
404
359
|
const wasExpired = this.lendingStatus.browsingExpired;
|
|
405
360
|
|
|
406
361
|
log('[IABookActions] BookReader:userAction received', {
|
|
@@ -408,7 +363,6 @@ export default class IABookActions extends LitElement {
|
|
|
408
363
|
browsingExpired: wasExpired,
|
|
409
364
|
});
|
|
410
365
|
|
|
411
|
-
// If the loan expired while the tab stayed visible, auto-renew on page turn
|
|
412
366
|
if (wasExpired) {
|
|
413
367
|
this.autoRenewExpiredLoan();
|
|
414
368
|
}
|
|
@@ -418,11 +372,8 @@ export default class IABookActions extends LitElement {
|
|
|
418
372
|
}
|
|
419
373
|
});
|
|
420
374
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
* so when user open that tab again,
|
|
424
|
-
* the [visibilitychange] event can trigger timer to show relavent messages
|
|
425
|
-
*/
|
|
375
|
+
// A tab in the background can have its intervals throttled/paused, so
|
|
376
|
+
// re-check status when the patron comes back to it.
|
|
426
377
|
document.addEventListener('visibilitychange', async () => {
|
|
427
378
|
if (document.hidden) {
|
|
428
379
|
log('[IABookActions] visibilitychange: tab backgrounded');
|
|
@@ -454,14 +405,9 @@ export default class IABookActions extends LitElement {
|
|
|
454
405
|
if (secondsLeft >= this.timerExecutionSeconds) {
|
|
455
406
|
this.loanStatusCheckInterval(Number(secondsLeft));
|
|
456
407
|
} 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.
|
|
408
|
+
// Loan expired while away — silently renew. Only clear
|
|
409
|
+
// intervals; disconnectedCallback() would drop the resize
|
|
410
|
+
// observer for a session that's still continuing.
|
|
465
411
|
window?.IALendingIntervals?.clearAll();
|
|
466
412
|
this.autoRenewExpiredLoan();
|
|
467
413
|
}
|
|
@@ -481,14 +427,8 @@ export default class IABookActions extends LitElement {
|
|
|
481
427
|
* @param {Boolean} hasPageChanged
|
|
482
428
|
*/
|
|
483
429
|
async autoLoanRenewChecker(hasPageChanged = false) {
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
// (e.g. a single scroll gesture), and this method has no debouncing of
|
|
487
|
-
// its own — without this guard, each event would spin up its own
|
|
488
|
-
// LoanRenewHelper concurrently, and whichever's async localCache reads
|
|
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.
|
|
430
|
+
// Re-entrancy guard against rapid BookReader:userAction events (e.g. a
|
|
431
|
+
// single scroll firing several in quick succession).
|
|
492
432
|
if (this.loanRenewInProgress) return;
|
|
493
433
|
|
|
494
434
|
this.loanRenewHelper = new LoanRenewHelper(
|
|
@@ -503,21 +443,15 @@ export default class IABookActions extends LitElement {
|
|
|
503
443
|
}
|
|
504
444
|
|
|
505
445
|
/**
|
|
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().
|
|
446
|
+
* Silently attempt to renew a browse loan that has expired, on
|
|
447
|
+
* visibilitychange or a page turn. handleLoanAutoRenewed()/
|
|
448
|
+
* handleLendingActionError() handle the outcome either way.
|
|
513
449
|
*/
|
|
514
450
|
autoRenewExpiredLoan() {
|
|
515
451
|
if (this.loanRenewInProgress) {
|
|
516
452
|
log(
|
|
517
453
|
'[IABookActions] autoRenewExpiredLoan: skipped, renewal already in progress',
|
|
518
|
-
{
|
|
519
|
-
identifier: this.identifier,
|
|
520
|
-
}
|
|
454
|
+
{ identifier: this.identifier }
|
|
521
455
|
);
|
|
522
456
|
return;
|
|
523
457
|
}
|
|
@@ -529,16 +463,8 @@ export default class IABookActions extends LitElement {
|
|
|
529
463
|
|
|
530
464
|
this.modal?.closeModal();
|
|
531
465
|
|
|
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.
|
|
466
|
+
// Optimistically flip browsingExpired so the bar stays in the reading
|
|
467
|
+
// state during the renew_loan round-trip, instead of showing "Borrow".
|
|
542
468
|
this.lendingStatus = {
|
|
543
469
|
...this.lendingStatus,
|
|
544
470
|
browsingExpired: false,
|
|
@@ -572,7 +498,6 @@ export default class IABookActions extends LitElement {
|
|
|
572
498
|
|
|
573
499
|
log('[IABookActions] showWarningModal');
|
|
574
500
|
|
|
575
|
-
// Capture texts and secondsLeft before resetting loanRenewResult
|
|
576
501
|
const {
|
|
577
502
|
texts: warningTexts,
|
|
578
503
|
secondsLeft: rawSecondsLeft,
|
|
@@ -584,7 +509,6 @@ export default class IABookActions extends LitElement {
|
|
|
584
509
|
secondsLeft = secondsLeft > 60 ? secondsLeft : 60;
|
|
585
510
|
}
|
|
586
511
|
|
|
587
|
-
// clear modal and reset renew state
|
|
588
512
|
this.modal.customModalContent = nothing;
|
|
589
513
|
this.modal?.closeModal();
|
|
590
514
|
this.loanRenewResult = { texts: '', renewNow: false };
|
|
@@ -666,9 +590,8 @@ export default class IABookActions extends LitElement {
|
|
|
666
590
|
'This book has been returned due to inactivity.';
|
|
667
591
|
|
|
668
592
|
this.modal?.closeModal();
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
// it again. Otherwise it stays latched for the life of the component.
|
|
593
|
+
// Release the warning-modal re-entrancy guard, or it stays latched for
|
|
594
|
+
// the life of the component.
|
|
672
595
|
this.warningModalOpen = false;
|
|
673
596
|
|
|
674
597
|
this.sentryCaptureMsg(sentryLogs.browseHasExpired);
|
|
@@ -776,31 +699,16 @@ export default class IABookActions extends LitElement {
|
|
|
776
699
|
}
|
|
777
700
|
|
|
778
701
|
/**
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
* - then change the remaining time
|
|
782
|
-
* - reset timer state
|
|
702
|
+
* Runs after a loan renewal completes (successfully or not): shows the
|
|
703
|
+
* outcome, updates the remaining time, and resets renewal-in-flight state.
|
|
783
704
|
* @param {object} event
|
|
784
705
|
*/
|
|
785
706
|
async handleLoanAutoRenewed({ detail }) {
|
|
786
707
|
const activeLoan = detail?.data?.loan;
|
|
787
708
|
|
|
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
|
-
*/
|
|
709
|
+
// Treat anything but a confirmed renewal as a failure, or
|
|
710
|
+
// loanRenewInProgress stays latched forever (autoLoanRenewChecker/
|
|
711
|
+
// autoRenewExpiredLoan both no-op on it).
|
|
804
712
|
if (!activeLoan?.renewal) {
|
|
805
713
|
this.loanRenewInProgress = false;
|
|
806
714
|
this.recoveringFromLoanExpiry = false;
|
|
@@ -816,7 +724,6 @@ export default class IABookActions extends LitElement {
|
|
|
816
724
|
}
|
|
817
725
|
|
|
818
726
|
if (this.loanRenewResult.renewNow) {
|
|
819
|
-
// Now, let's reset loan duration & this.lendingStatus
|
|
820
727
|
const loanTime = await this.localCache.get(`${this.identifier}-loanTime`);
|
|
821
728
|
|
|
822
729
|
// number of seconds left in current loan
|
|
@@ -834,19 +741,8 @@ export default class IABookActions extends LitElement {
|
|
|
834
741
|
});
|
|
835
742
|
|
|
836
743
|
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.
|
|
744
|
+
// Only now is it safe to re-initialize BookReader — not for a
|
|
745
|
+
// routine top-up, where that would be disruptive.
|
|
850
746
|
this.postInitComplete = false;
|
|
851
747
|
}
|
|
852
748
|
|
|
@@ -870,46 +766,34 @@ export default class IABookActions extends LitElement {
|
|
|
870
766
|
this.loanRenewInProgress = false;
|
|
871
767
|
}
|
|
872
768
|
|
|
873
|
-
/**
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
* @memberof IABookActions
|
|
877
|
-
*/
|
|
769
|
+
/** Start the countdown interval; ticks read the live secondsLeftOnLoan
|
|
770
|
+
* each time rather than a value frozen at start, so drift can't compound
|
|
771
|
+
* across ticks (see loanStatusCheckInterval). */
|
|
878
772
|
async startTimerCountdown() {
|
|
879
773
|
window?.IALendingIntervals?.clearTimerCountdown();
|
|
880
|
-
|
|
881
|
-
const secondsLeft = Number(this.lendingStatus.secondsLeftOnLoan);
|
|
882
774
|
this.timeWhenTimerStart = new Date();
|
|
883
775
|
|
|
884
776
|
window.IALendingIntervals.timerCountdown = setInterval(async () => {
|
|
885
|
-
|
|
886
|
-
|
|
777
|
+
await this.loanStatusCheckInterval(
|
|
778
|
+
Number(this.lendingStatus.secondsLeftOnLoan)
|
|
779
|
+
);
|
|
887
780
|
}, this.timerExecutionSeconds * 1000);
|
|
888
781
|
}
|
|
889
782
|
|
|
890
783
|
/**
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
* - loan-renew attempt to renew the loan OR show relavant message
|
|
894
|
-
* - if loan has expired, clear interval timers
|
|
895
|
-
*
|
|
784
|
+
* Runs on every countdown tick: resyncs against wall-clock time, attempts
|
|
785
|
+
* a renewal near expiry, and clears the timers once the loan expires.
|
|
896
786
|
* @param {Number} secondsLeftOnLoan
|
|
897
787
|
*/
|
|
898
788
|
async loanStatusCheckInterval(secondsLeftOnLoan) {
|
|
899
789
|
let secondsLeft = secondsLeftOnLoan;
|
|
900
790
|
secondsLeft -= this.timerExecutionSeconds;
|
|
901
|
-
secondsLeft = Math.round(secondsLeft);
|
|
791
|
+
secondsLeft = Math.round(secondsLeft);
|
|
902
792
|
|
|
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
793
|
const resyncd = this.reSyncTimerIfGoneOff(secondsLeft);
|
|
907
|
-
|
|
908
794
|
if (resyncd.hasSynced) {
|
|
909
795
|
secondsLeft = resyncd.whatShouldLeft;
|
|
910
|
-
log('[IABookActions] timer: timer re-synced', {
|
|
911
|
-
secondsLeft,
|
|
912
|
-
});
|
|
796
|
+
log('[IABookActions] timer: timer re-synced', { secondsLeft });
|
|
913
797
|
}
|
|
914
798
|
|
|
915
799
|
log('[IABookActions] timer', {
|
|
@@ -917,19 +801,20 @@ export default class IABookActions extends LitElement {
|
|
|
917
801
|
whatIsleft: secondsLeft,
|
|
918
802
|
});
|
|
919
803
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
804
|
+
// Re-anchor every tick so drift from wall-clock time can't compound
|
|
805
|
+
// across ticks that don't happen to trigger a resync above.
|
|
806
|
+
this.timeWhenTimerStart = new Date();
|
|
807
|
+
this.lendingStatus = { ...this.lendingStatus, secondsLeftOnLoan: secondsLeft };
|
|
808
|
+
|
|
809
|
+
// 10 minutes out: start checking for a renewal. 0: show the "about to
|
|
810
|
+
// auto-return" warning. @see IABookActions::bindLoanRenewEvents
|
|
926
811
|
if (secondsLeft <= this.loanRenewTimeConfig.loanRenewAtLast) {
|
|
927
812
|
await this.loanRenewAttempt(secondsLeft);
|
|
928
813
|
}
|
|
929
814
|
|
|
930
|
-
// clear interval in secondsLeft if less
|
|
931
815
|
if (secondsLeft <= this.timerExecutionSeconds) {
|
|
932
|
-
|
|
816
|
+
window?.IALendingIntervals?.clearAll();
|
|
817
|
+
this.tokenPoller?.disconnectedCallback();
|
|
933
818
|
this.sentryCaptureMsg(sentryLogs.clearOneHourTimer);
|
|
934
819
|
}
|
|
935
820
|
}
|
|
@@ -984,13 +869,8 @@ export default class IABookActions extends LitElement {
|
|
|
984
869
|
*/
|
|
985
870
|
async loanRenewAttempt(secondsLeft) {
|
|
986
871
|
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
|
-
*/
|
|
872
|
+
// Under 50s left there isn't enough time for the renew_loan round-trip
|
|
873
|
+
// and create_token to load images, so just expire the loan.
|
|
994
874
|
if (loanSecondsLeft < 50) {
|
|
995
875
|
log('[IABookActions] loanRenewAttempt: < 50s left, expiring loan');
|
|
996
876
|
await this.browseHasExpired();
|
|
@@ -999,17 +879,13 @@ export default class IABookActions extends LitElement {
|
|
|
999
879
|
|
|
1000
880
|
await this.autoLoanRenewChecker(false);
|
|
1001
881
|
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
1004
|
-
// tick — only an actual renewal (see handleLoanAutoRenewed) re-arms it.
|
|
882
|
+
// Once dismissed, don't re-show the warning on every subsequent tick —
|
|
883
|
+
// only an actual renewal (handleLoanAutoRenewed) re-arms it.
|
|
1005
884
|
if (
|
|
1006
885
|
this.loanRenewResult.renewNow === false &&
|
|
1007
886
|
!this.warningModalDismissed
|
|
1008
887
|
) {
|
|
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
|
-
*/
|
|
888
|
+
// Compensate for the 50s buffer above by warning a minute early.
|
|
1013
889
|
loanSecondsLeft -= 60;
|
|
1014
890
|
this.loanRenewResult.secondsLeft = loanSecondsLeft;
|
|
1015
891
|
|
|
@@ -1032,17 +908,9 @@ export default class IABookActions extends LitElement {
|
|
|
1032
908
|
this.handleLendingActionError(eventObj);
|
|
1033
909
|
};
|
|
1034
910
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
* 1. this.identifier
|
|
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
|
-
*/
|
|
911
|
+
// Tear down any previous poller first, so only one is ever controlling
|
|
912
|
+
// window.IALendingIntervals.tokenPoller at a time.
|
|
913
|
+
this.tokenPoller?.disconnectedCallback();
|
|
1046
914
|
this.tokenPoller = new LoanTokenPoller(
|
|
1047
915
|
this.identifier,
|
|
1048
916
|
this.borrowType,
|
|
@@ -1062,13 +930,12 @@ export default class IABookActions extends LitElement {
|
|
|
1062
930
|
}
|
|
1063
931
|
|
|
1064
932
|
/**
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
933
|
+
* Handles lending errors from any action (browse_book, borrow_book,
|
|
934
|
+
* create_token, renew_loan, etc).
|
|
1068
935
|
* @event IABookActions#lendingActionError
|
|
1069
|
-
* @param {Object} event
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
936
|
+
* @param {Object} event
|
|
937
|
+
* @param {string} event.detail.action
|
|
938
|
+
* @param {string} event.detail.data.error
|
|
1072
939
|
*/
|
|
1073
940
|
handleLendingActionError(event) {
|
|
1074
941
|
this.disableActionGroup = false;
|
|
@@ -1087,42 +954,29 @@ export default class IABookActions extends LitElement {
|
|
|
1087
954
|
});
|
|
1088
955
|
|
|
1089
956
|
if (action === 'create_token') {
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1092
|
-
//
|
|
1093
|
-
//
|
|
1094
|
-
window?.IALendingIntervals?.clearTokenPoller();
|
|
1095
|
-
|
|
1096
|
-
/**
|
|
1097
|
-
* Only the INITIAL token matters to the patron: without it BookReader
|
|
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
|
-
*/
|
|
957
|
+
// Only the INITIAL token matters enough to interrupt the patron — a
|
|
958
|
+
// routine interval refresh stays silent; the pages already loaded
|
|
959
|
+
// are still served, and the next scheduled poll tries again on its
|
|
960
|
+
// own without any special retry.
|
|
1107
961
|
if (!isInitial) {
|
|
1108
962
|
log(
|
|
1109
|
-
'[IABookActions] create_token failed on interval refresh — staying silent
|
|
963
|
+
'[IABookActions] create_token failed on interval refresh — staying silent'
|
|
1110
964
|
);
|
|
1111
965
|
return;
|
|
1112
966
|
}
|
|
1113
967
|
|
|
1114
|
-
// The
|
|
1115
|
-
//
|
|
1116
|
-
//
|
|
968
|
+
// The book never opened, and we don't retry — reset the bar to
|
|
969
|
+
// Borrow and the timer to 0 rather than leaving a red "Return now"
|
|
970
|
+
// with a stale countdown for a session that isn't accessible.
|
|
971
|
+
window?.IALendingIntervals?.clearAll();
|
|
972
|
+
this.tokenPoller?.disconnectedCallback();
|
|
1117
973
|
this.lendingStatus = {
|
|
1118
974
|
...this.lendingStatus,
|
|
1119
975
|
user_has_browsed: false,
|
|
1120
976
|
available_to_browse: true,
|
|
977
|
+
secondsLeftOnLoan: 0,
|
|
1121
978
|
};
|
|
1122
979
|
|
|
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
980
|
// showErrorModal has dedicated create_token messaging (refresh
|
|
1127
981
|
// button + support email).
|
|
1128
982
|
if (errorMsg) this.showErrorModal(errorMsg, action);
|
|
@@ -1131,11 +985,8 @@ export default class IABookActions extends LitElement {
|
|
|
1131
985
|
this.loanRenewInProgress = false;
|
|
1132
986
|
this.recoveringFromLoanExpiry = false;
|
|
1133
987
|
|
|
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.
|
|
988
|
+
// The loan was NOT renewed — reflect that immediately (show Borrow,
|
|
989
|
+
// clear the stale timer) rather than leaving a frozen "Return now".
|
|
1139
990
|
this.lendingStatus = {
|
|
1140
991
|
...this.lendingStatus,
|
|
1141
992
|
user_has_browsed: false,
|
|
@@ -1143,11 +994,8 @@ export default class IABookActions extends LitElement {
|
|
|
1143
994
|
secondsLeftOnLoan: 0,
|
|
1144
995
|
};
|
|
1145
996
|
|
|
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).
|
|
997
|
+
// Refresh the page on dismissal so the client picks up whatever the
|
|
998
|
+
// server now authoritatively considers true (any error message).
|
|
1151
999
|
this.showLoanUnavailableModal(errorMsg);
|
|
1152
1000
|
} else {
|
|
1153
1001
|
// Every other action failure genuinely affects loan state.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { expect
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
2
|
import Sinon from 'sinon';
|
|
3
3
|
|
|
4
4
|
import { LoanTokenPoller } from '../../../src/core/services/loan-token-poller.js';
|
|
@@ -57,15 +57,9 @@ describe('Get Loan Token', () => {
|
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
describe('handleTokenError
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// right after a renewal can race that write's propagation and come
|
|
64
|
-
// back with this exact error even though the loan is genuinely valid.
|
|
65
|
-
const staleLoanReadError = {
|
|
66
|
-
error: 'You do not currently have this book borrowed.',
|
|
67
|
-
};
|
|
68
|
-
|
|
60
|
+
describe('handleTokenError (no retry)', () => {
|
|
61
|
+
// A create_token failure is treated as terminal on the first attempt —
|
|
62
|
+
// no retry, whether it's the "stale loan read" race or anything else.
|
|
69
63
|
const makeTokenPoller = () =>
|
|
70
64
|
new LoanTokenPoller(
|
|
71
65
|
'identifier1',
|
|
@@ -75,126 +69,43 @@ describe('handleTokenError retry (WEBDEV-8322 follow-up)', () => {
|
|
|
75
69
|
2000
|
|
76
70
|
);
|
|
77
71
|
|
|
78
|
-
it('
|
|
72
|
+
it('reports the error immediately, without retrying, on the initial call', () => {
|
|
79
73
|
const tokenPoller = makeTokenPoller();
|
|
80
74
|
const handleLoanTokenPollerSpy = Sinon.spy(
|
|
81
75
|
tokenPoller,
|
|
82
76
|
'handleLoanTokenPoller'
|
|
83
77
|
);
|
|
84
78
|
const errorCallbackSpy = Sinon.spy(tokenPoller, 'errorCallback');
|
|
85
|
-
const clock = Sinon.useFakeTimers();
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
tokenPoller.handleTokenError(staleLoanReadError, true, 0);
|
|
89
|
-
|
|
90
|
-
expect(errorCallbackSpy.called).to.be.false;
|
|
91
|
-
|
|
92
|
-
clock.tick(tokenPoller.tokenRetryDelay);
|
|
93
|
-
|
|
94
|
-
expect(handleLoanTokenPollerSpy.calledWith(true, 1)).to.be.true;
|
|
95
|
-
} finally {
|
|
96
|
-
clock.restore();
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('gives up and calls errorCallback after exhausting retries', () => {
|
|
101
|
-
const tokenPoller = makeTokenPoller();
|
|
102
|
-
const errorCallbackSpy = Sinon.spy(tokenPoller, 'errorCallback');
|
|
103
79
|
|
|
104
80
|
tokenPoller.handleTokenError(
|
|
105
|
-
|
|
106
|
-
true
|
|
107
|
-
tokenPoller.maxTokenRetries
|
|
81
|
+
{ error: 'You do not currently have this book borrowed.' },
|
|
82
|
+
true
|
|
108
83
|
);
|
|
109
84
|
|
|
110
85
|
expect(errorCallbackSpy.calledOnce).to.be.true;
|
|
86
|
+
expect(handleLoanTokenPollerSpy.called).to.be.false;
|
|
111
87
|
});
|
|
112
88
|
|
|
113
|
-
it('
|
|
114
|
-
// A routine tick landing moments after a renewal hits the very same
|
|
115
|
-
// write-propagation race as the initial call, so it's equally worth
|
|
116
|
-
// retrying. isInitial only decides how a FINAL failure is reported.
|
|
117
|
-
const tokenPoller = makeTokenPoller();
|
|
118
|
-
const handleLoanTokenPollerSpy = Sinon.spy(
|
|
119
|
-
tokenPoller,
|
|
120
|
-
'handleLoanTokenPoller'
|
|
121
|
-
);
|
|
122
|
-
const errorCallbackSpy = Sinon.spy(tokenPoller, 'errorCallback');
|
|
123
|
-
const clock = Sinon.useFakeTimers();
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
tokenPoller.handleTokenError(staleLoanReadError, false, 0);
|
|
127
|
-
|
|
128
|
-
expect(errorCallbackSpy.called).to.be.false;
|
|
129
|
-
|
|
130
|
-
clock.tick(tokenPoller.tokenRetryDelay);
|
|
131
|
-
|
|
132
|
-
expect(handleLoanTokenPollerSpy.calledWith(false, 1)).to.be.true;
|
|
133
|
-
} finally {
|
|
134
|
-
clock.restore();
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('does not retry a genuinely different error, even on the initial call', () => {
|
|
89
|
+
it('reports the error immediately on a routine (non-initial) poll too', () => {
|
|
139
90
|
const tokenPoller = makeTokenPoller();
|
|
140
91
|
const errorCallbackSpy = Sinon.spy(tokenPoller, 'errorCallback');
|
|
141
92
|
|
|
142
93
|
tokenPoller.handleTokenError(
|
|
143
|
-
{ error: '
|
|
144
|
-
|
|
145
|
-
0
|
|
94
|
+
{ error: 'You do not currently have this book borrowed.' },
|
|
95
|
+
false
|
|
146
96
|
);
|
|
147
97
|
|
|
148
98
|
expect(errorCallbackSpy.calledOnce).to.be.true;
|
|
149
99
|
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
describe('LoanTokenPoller - WEBDEV-8322 review fixes', () => {
|
|
153
|
-
it('retries a stale-loan-read error on interval refreshes too, not just the initial call', async () => {
|
|
154
|
-
const poller = new LoanTokenPoller('foo', 'browsed', () => {}, () => {}, 120);
|
|
155
|
-
poller.disconnectedCallback(); // stop the real poller started in the ctor
|
|
156
|
-
|
|
157
|
-
const retrySpy = Sinon.stub(poller, 'handleLoanTokenPoller');
|
|
158
|
-
poller.tokenRetryDelay = 1;
|
|
159
|
-
|
|
160
|
-
poller.handleTokenError(
|
|
161
|
-
{ error: 'you do not currently have this book borrowed' },
|
|
162
|
-
false, // interval refresh
|
|
163
|
-
0
|
|
164
|
-
);
|
|
165
|
-
await aTimeout(30);
|
|
166
|
-
|
|
167
|
-
expect(retrySpy.calledOnce).to.be.true;
|
|
168
|
-
expect(retrySpy.firstCall.args).to.deep.equal([false, 1]);
|
|
169
|
-
poller.disconnectedCallback();
|
|
170
|
-
});
|
|
171
100
|
|
|
172
101
|
it('passes isInitial through to the error callback', () => {
|
|
173
102
|
const errorCallback = Sinon.spy();
|
|
174
103
|
const poller = new LoanTokenPoller('foo', 'browsed', () => {}, errorCallback, 120);
|
|
175
104
|
poller.disconnectedCallback();
|
|
176
105
|
|
|
177
|
-
poller.handleTokenError({ error: 'something else went wrong' }, true
|
|
106
|
+
poller.handleTokenError({ error: 'something else went wrong' }, true);
|
|
178
107
|
|
|
179
108
|
expect(errorCallback.calledOnce).to.be.true;
|
|
180
109
|
expect(errorCallback.firstCall.args[0].detail.isInitial).to.be.true;
|
|
181
110
|
});
|
|
182
|
-
|
|
183
|
-
it('cancels a pending retry on teardown', async () => {
|
|
184
|
-
const poller = new LoanTokenPoller('foo', 'browsed', () => {}, () => {}, 120);
|
|
185
|
-
const retrySpy = Sinon.stub(poller, 'handleLoanTokenPoller');
|
|
186
|
-
poller.tokenRetryDelay = 20;
|
|
187
|
-
|
|
188
|
-
poller.handleTokenError(
|
|
189
|
-
{ error: 'you do not currently have this book borrowed' },
|
|
190
|
-
true,
|
|
191
|
-
0
|
|
192
|
-
);
|
|
193
|
-
expect(poller.retryTimeout).to.not.be.undefined;
|
|
194
|
-
|
|
195
|
-
poller.disconnectedCallback();
|
|
196
|
-
await aTimeout(60);
|
|
197
|
-
|
|
198
|
-
expect(retrySpy.called).to.be.false;
|
|
199
|
-
});
|
|
200
111
|
});
|
|
@@ -1101,14 +1101,11 @@ describe('handleLendingActionError - create_token failures must not stop the cou
|
|
|
1101
1101
|
expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
|
|
1102
1102
|
});
|
|
1103
1103
|
|
|
1104
|
-
it('shows the error modal
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
// that
|
|
1109
|
-
// won't open — so the patron must be told rather than left staring
|
|
1110
|
-
// at nothing. (An interval refresh failing is handled separately and
|
|
1111
|
-
// stays silent; see the WEBDEV-8322 review fixes below.)
|
|
1104
|
+
it('shows the error modal and resets to Borrow/0 on a failed INITIAL create_token (no retry)', async () => {
|
|
1105
|
+
// For the INITIAL token, BookReader never got page images — the book
|
|
1106
|
+
// never opened. We don't retry, so the bar must reflect that honestly
|
|
1107
|
+
// (Borrow, 0 left) instead of staying red with a stale countdown for a
|
|
1108
|
+
// session that isn't actually accessible.
|
|
1112
1109
|
const el = await fixture(
|
|
1113
1110
|
container({
|
|
1114
1111
|
userid: '@user1',
|
|
@@ -1136,6 +1133,40 @@ describe('handleLendingActionError - create_token failures must not stop the cou
|
|
|
1136
1133
|
expect(showErrorModalSpy.calledOnceWith(errorMsg, 'create_token')).to.be
|
|
1137
1134
|
.true;
|
|
1138
1135
|
expect(el.lendingStatus.user_has_browsed).to.be.false;
|
|
1136
|
+
expect(el.lendingStatus.available_to_browse).to.be.true;
|
|
1137
|
+
expect(el.lendingStatus.secondsLeftOnLoan).to.equal(0);
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it('stops the countdown/expiry timers on a failed INITIAL create_token — nothing to read yet', async () => {
|
|
1141
|
+
// Unlike the interval-refresh case above (pages already loaded, keep
|
|
1142
|
+
// ticking), an initial create_token failure means BookReader never got
|
|
1143
|
+
// page images and we're resetting the bar to Borrow — both timers
|
|
1144
|
+
// should stop rather than keep counting down a dead session.
|
|
1145
|
+
const el = await fixture(
|
|
1146
|
+
container({
|
|
1147
|
+
userid: '@user1',
|
|
1148
|
+
identifier: 'foobar',
|
|
1149
|
+
lendingStatus: {
|
|
1150
|
+
user_has_browsed: true,
|
|
1151
|
+
browsingExpired: false,
|
|
1152
|
+
secondsLeftOnLoan: 100,
|
|
1153
|
+
},
|
|
1154
|
+
})
|
|
1155
|
+
);
|
|
1156
|
+
await el.updateComplete;
|
|
1157
|
+
expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
|
|
1158
|
+
expect(window.IALendingIntervals.browseExpireTimeout).to.not.equal(0);
|
|
1159
|
+
|
|
1160
|
+
el.handleLendingActionError({
|
|
1161
|
+
detail: {
|
|
1162
|
+
action: 'create_token',
|
|
1163
|
+
isInitial: true,
|
|
1164
|
+
data: { error: 'loan token not found. please try again later.' },
|
|
1165
|
+
},
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
expect(window.IALendingIntervals.timerCountdown).to.equal(0);
|
|
1169
|
+
expect(window.IALendingIntervals.browseExpireTimeout).to.equal(0);
|
|
1139
1170
|
});
|
|
1140
1171
|
|
|
1141
1172
|
it('still clears everything on a renew_loan failure', async () => {
|
|
@@ -1511,11 +1542,14 @@ describe('WEBDEV-8322 review fixes', () => {
|
|
|
1511
1542
|
expect(el.lendingStatus.user_has_browsed).to.be.true;
|
|
1512
1543
|
});
|
|
1513
1544
|
|
|
1514
|
-
it('
|
|
1545
|
+
it('tears down the previous poller before starting a new one', async () => {
|
|
1546
|
+
// Regression: startLoanTokenPoller() always replaced this.tokenPoller
|
|
1547
|
+
// without cancelling the old instance's pending internal retry timer,
|
|
1548
|
+
// which could later fire against stale state.
|
|
1515
1549
|
const el = await fixture(
|
|
1516
1550
|
container({
|
|
1517
1551
|
userid: '@user1',
|
|
1518
|
-
identifier: '
|
|
1552
|
+
identifier: 'poller-teardown',
|
|
1519
1553
|
lendingStatus: {
|
|
1520
1554
|
is_lendable: true,
|
|
1521
1555
|
available_to_browse: true,
|
|
@@ -1526,7 +1560,34 @@ describe('WEBDEV-8322 review fixes', () => {
|
|
|
1526
1560
|
})
|
|
1527
1561
|
);
|
|
1528
1562
|
await el.updateComplete;
|
|
1529
|
-
|
|
1563
|
+
|
|
1564
|
+
el.startLoanTokenPoller();
|
|
1565
|
+
const firstPoller = el.tokenPoller;
|
|
1566
|
+
const disconnectSpy = Sinon.spy(firstPoller, 'disconnectedCallback');
|
|
1567
|
+
|
|
1568
|
+
el.startLoanTokenPoller();
|
|
1569
|
+
|
|
1570
|
+
expect(disconnectSpy.calledOnce).to.be.true;
|
|
1571
|
+
expect(el.tokenPoller).to.not.equal(firstPoller);
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
it('never restarts the poller on a create_token failure — no retry', async () => {
|
|
1575
|
+
const el = await fixture(
|
|
1576
|
+
container({
|
|
1577
|
+
userid: '@user1',
|
|
1578
|
+
identifier: 'token-exhausted',
|
|
1579
|
+
lendingStatus: {
|
|
1580
|
+
is_lendable: true,
|
|
1581
|
+
available_to_browse: true,
|
|
1582
|
+
user_has_browsed: true,
|
|
1583
|
+
browsingExpired: false,
|
|
1584
|
+
secondsLeftOnLoan: 300,
|
|
1585
|
+
},
|
|
1586
|
+
})
|
|
1587
|
+
);
|
|
1588
|
+
await el.updateComplete;
|
|
1589
|
+
|
|
1590
|
+
const startSpy = Sinon.spy(el, 'startLoanTokenPoller');
|
|
1530
1591
|
|
|
1531
1592
|
el.handleLendingActionError({
|
|
1532
1593
|
detail: {
|
|
@@ -1535,9 +1596,7 @@ describe('WEBDEV-8322 review fixes', () => {
|
|
|
1535
1596
|
data: { error: 'loan token not found. please try again later.' },
|
|
1536
1597
|
},
|
|
1537
1598
|
});
|
|
1538
|
-
await el.updateComplete;
|
|
1539
1599
|
|
|
1540
|
-
expect(
|
|
1541
|
-
expect(el.lendingStatus.user_has_browsed).to.be.false;
|
|
1600
|
+
expect(startSpy.called).to.be.false;
|
|
1542
1601
|
});
|
|
1543
1602
|
});
|