@internetarchive/ia-book-actions 1.4.4-alpha9 → 1.4.5
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/assets/data/info-icon.js +17 -0
- package/src/core/services/actions-handler/actions-handler-service.js +27 -4
- package/src/core/services/actions-handler/actions-handler.js +77 -36
- package/src/core/services/loan-analytics.js +0 -11
- package/src/core/services/loan-renew-helper.js +2 -2
- package/src/core/services/loan-token-poller.js +87 -19
- package/src/ia-book-actions.js +290 -245
- package/test/core/services/loan-token-poller.test.js +111 -15
- package/test/ia-book-actions.test.js +503 -35
- package/src/assets/data/info.js +0 -12
package/src/ia-book-actions.js
CHANGED
|
@@ -20,7 +20,7 @@ import { LoanTokenPoller } from './core/services/loan-token-poller.js';
|
|
|
20
20
|
import { LoanRenewHelper } from './core/services/loan-renew-helper.js';
|
|
21
21
|
import log from './core/services/log.js';
|
|
22
22
|
import { URLHelper } from './core/config/url-helper.js';
|
|
23
|
-
import { infoIcon } from './assets/data/info.js';
|
|
23
|
+
import { infoIcon } from './assets/data/info-icon.js';
|
|
24
24
|
|
|
25
25
|
export const events = {
|
|
26
26
|
browseExpired: 'IABookReader:BrowsingHasExpired',
|
|
@@ -55,12 +55,15 @@ export default class IABookActions extends LitElement {
|
|
|
55
55
|
type: Function,
|
|
56
56
|
attribute: false,
|
|
57
57
|
},
|
|
58
|
+
reloadPageImages: {
|
|
59
|
+
type: Function,
|
|
60
|
+
attribute: false,
|
|
61
|
+
},
|
|
58
62
|
barType: { type: String },
|
|
59
63
|
sharedObserver: { attribute: false },
|
|
60
64
|
disableActionGroup: { type: Boolean },
|
|
61
65
|
modal: { Object },
|
|
62
66
|
tokenDelay: { type: Number },
|
|
63
|
-
tokenPollerRenewalDelay: { type: Number },
|
|
64
67
|
timerExecutionSeconds: { type: Number },
|
|
65
68
|
localCache: { type: Object },
|
|
66
69
|
loanRenewTimeConfig: { type: Object },
|
|
@@ -78,6 +81,7 @@ export default class IABookActions extends LitElement {
|
|
|
78
81
|
this.width = 0;
|
|
79
82
|
this.bwbPurchaseUrl = '';
|
|
80
83
|
this.lendingBarPostInit = () => {};
|
|
84
|
+
this.reloadPageImages = () => {};
|
|
81
85
|
this.barType = 'action'; // 'title'|'action'
|
|
82
86
|
this.sharedObserver = undefined;
|
|
83
87
|
this.disableActionGroup = false;
|
|
@@ -98,34 +102,26 @@ export default class IABookActions extends LitElement {
|
|
|
98
102
|
this.loanRenewInProgress = false;
|
|
99
103
|
|
|
100
104
|
/**
|
|
101
|
-
* True while
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* re-initialized once the renewal is confirmed — see
|
|
105
|
+
* True while recovering from a loan that genuinely lapsed (set by
|
|
106
|
+
* autoRenewExpiredLoan), as opposed to a routine pre-expiry top-up.
|
|
107
|
+
* Only the recovery case needs BookReader re-initialized — see
|
|
105
108
|
* handleLoanAutoRenewed.
|
|
106
109
|
*/
|
|
107
110
|
this.recoveringFromLoanExpiry = false;
|
|
108
111
|
|
|
109
112
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* create_token
|
|
114
|
-
*
|
|
115
|
-
* configurable per-deployment and overridable to 0 in tests.
|
|
113
|
+
* True right after a confirmed renewal (any kind), until the token
|
|
114
|
+
* poller restarts. renew_loan's own response already carries a valid
|
|
115
|
+
* access token, so that restart should skip its normal immediate
|
|
116
|
+
* create_token call — see handleLoanAutoRenewed/
|
|
117
|
+
* setupLendingToolbarActions.
|
|
116
118
|
*/
|
|
117
|
-
this.
|
|
119
|
+
this.skipNextInitialTokenCall = false;
|
|
118
120
|
|
|
119
121
|
this.warningModalOpen = false;
|
|
120
122
|
|
|
121
|
-
/**
|
|
122
|
-
*
|
|
123
|
-
* don't nag them again with the same modal every timer tick — only a
|
|
124
|
-
* real renewal (via interacting with the book) clears this, giving the
|
|
125
|
-
* next pre-expiry window its own single warning.
|
|
126
|
-
* @see loanRenewAttempt
|
|
127
|
-
* @see handleLoanAutoRenewed
|
|
128
|
-
*/
|
|
123
|
+
/** Once dismissed, don't re-show the warning modal every tick — only
|
|
124
|
+
* a real renewal (handleLoanAutoRenewed) re-arms it. */
|
|
129
125
|
this.warningModalDismissed = false;
|
|
130
126
|
|
|
131
127
|
/**
|
|
@@ -159,9 +155,9 @@ export default class IABookActions extends LitElement {
|
|
|
159
155
|
}
|
|
160
156
|
|
|
161
157
|
disconnectedCallback() {
|
|
162
|
-
|
|
158
|
+
super.disconnectedCallback();
|
|
163
159
|
window?.IALendingIntervals?.clearAll();
|
|
164
|
-
|
|
160
|
+
this.tokenPoller?.disconnectedCallback();
|
|
165
161
|
this.sentryCaptureMsg(sentryLogs.disconnectedCallback);
|
|
166
162
|
this.disconnectResizeObserver();
|
|
167
163
|
}
|
|
@@ -200,12 +196,8 @@ export default class IABookActions extends LitElement {
|
|
|
200
196
|
}
|
|
201
197
|
|
|
202
198
|
if (changed.has('loanRenewResult') && this.loanRenewResult.renewNow) {
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
// so every path (interaction during active reading, periodic
|
|
206
|
-
// checker, auto-return recovery) gets the same "renewing" signal
|
|
207
|
-
// instead of just looking frozen until handleLoanAutoRenewed() clears
|
|
208
|
-
// this and restarts the timer.
|
|
199
|
+
// Pause the countdown for the renew_loan round-trip, for every
|
|
200
|
+
// renewal path — cleared by handleLoanAutoRenewed().
|
|
209
201
|
this.loanRenewInProgress = true;
|
|
210
202
|
window.IALendingIntervals.clearAll();
|
|
211
203
|
}
|
|
@@ -243,17 +235,53 @@ export default class IABookActions extends LitElement {
|
|
|
243
235
|
}
|
|
244
236
|
/** End SharedObserver resize handler */
|
|
245
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Recompute the action bar (title, buttons, colors, borrowType) from the
|
|
240
|
+
* current lendingStatus.
|
|
241
|
+
*
|
|
242
|
+
* @returns {boolean} false when there are no actions to apply, in which
|
|
243
|
+
* case callers should bail out rather than continue with stale state.
|
|
244
|
+
*/
|
|
245
|
+
applyLendingActions() {
|
|
246
|
+
this.lendingOptions = new GetLendingActions(
|
|
247
|
+
this.userid,
|
|
248
|
+
this.identifier,
|
|
249
|
+
this.lendingStatus,
|
|
250
|
+
this.bwbPurchaseUrl
|
|
251
|
+
);
|
|
252
|
+
const actions = this.lendingOptions.getCurrentLendingActions();
|
|
253
|
+
|
|
254
|
+
if (!actions) return false;
|
|
255
|
+
|
|
256
|
+
this.primaryTitle = actions.primaryTitle;
|
|
257
|
+
this.primaryActions = actions.primaryActions?.filter(action => {
|
|
258
|
+
return action != null;
|
|
259
|
+
});
|
|
260
|
+
this.primaryColor = actions.primaryColor;
|
|
261
|
+
this.secondaryActions = actions.secondaryActions?.filter(action => {
|
|
262
|
+
return action != null;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
this.borrowType = actions.borrowType ? actions.borrowType : null;
|
|
266
|
+
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
246
270
|
async setupLendingToolbarActions() {
|
|
247
271
|
const hasExpired =
|
|
248
272
|
'browsingExpired' in this.lendingStatus &&
|
|
249
273
|
this.lendingStatus?.browsingExpired;
|
|
250
274
|
if (hasExpired) {
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
275
|
+
// Auto-return must not visibly change the bar — unless nothing's
|
|
276
|
+
// rendered yet (fresh load of an already-expired loan).
|
|
277
|
+
if (this.primaryActions?.length) {
|
|
278
|
+
log('[IABookActions] browsing expired — leaving action bar untouched');
|
|
279
|
+
} else {
|
|
280
|
+
log(
|
|
281
|
+
'[IABookActions] browsing expired on first render — populating action bar'
|
|
282
|
+
);
|
|
283
|
+
this.applyLendingActions();
|
|
284
|
+
}
|
|
257
285
|
|
|
258
286
|
if (!this.tokenPoller) {
|
|
259
287
|
this.sentryCaptureMsg(sentryLogs.bookWasExpired);
|
|
@@ -273,32 +301,20 @@ export default class IABookActions extends LitElement {
|
|
|
273
301
|
return;
|
|
274
302
|
}
|
|
275
303
|
|
|
276
|
-
this.
|
|
277
|
-
this.userid,
|
|
278
|
-
this.identifier,
|
|
279
|
-
this.lendingStatus,
|
|
280
|
-
this.bwbPurchaseUrl
|
|
281
|
-
);
|
|
282
|
-
const actions = this.lendingOptions.getCurrentLendingActions();
|
|
283
|
-
|
|
284
|
-
if (!actions) return;
|
|
304
|
+
if (!this.applyLendingActions()) return;
|
|
285
305
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
this.borrowType = actions.borrowType ? actions.borrowType : null;
|
|
306
|
+
// primaryActions/borrowType/etc are plain fields, not reactive
|
|
307
|
+
// properties, so nothing pushes them to the rendered template (and
|
|
308
|
+
// from there to <collapsible-action-group>) on its own. Do that now,
|
|
309
|
+
// unconditionally — the early returns below (not borrowed, title bar)
|
|
310
|
+
// must not skip it, or the action bar visibly goes stale (e.g. a
|
|
311
|
+
// create_token failure resetting to Borrow never actually shows it).
|
|
312
|
+
this.requestUpdate();
|
|
296
313
|
|
|
297
|
-
|
|
298
|
-
|
|
314
|
+
// Don't (re)start the countdown mid-renewal — secondsLeftOnLoan could
|
|
315
|
+
// still be stale until handleLoanAutoRenewed() confirms it.
|
|
316
|
+
if (this.borrowType === 'browsed' && !this.loanRenewInProgress) {
|
|
299
317
|
await this.startTimerCountdown();
|
|
300
|
-
|
|
301
|
-
// start timer for browsed.
|
|
302
318
|
await this.startBrowseTimer();
|
|
303
319
|
}
|
|
304
320
|
|
|
@@ -308,40 +324,35 @@ export default class IABookActions extends LitElement {
|
|
|
308
324
|
return;
|
|
309
325
|
}
|
|
310
326
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
* - if book is going to renew, need to wait until renew is completed:
|
|
314
|
-
* loanRenewInProgress is set before renew_loan is even dispatched
|
|
315
|
-
* (including the optimistic pre-confirmation window) and only
|
|
316
|
-
* clears once the renewal is confirmed, so starting the poller here
|
|
317
|
-
* mid-renewal would call create_token against the loan record
|
|
318
|
-
* before it's actually renewed.
|
|
319
|
-
*/
|
|
327
|
+
// Wait until any in-flight renewal is confirmed before (re)starting the
|
|
328
|
+
// poller, so create_token isn't called against a not-yet-renewed loan.
|
|
320
329
|
setTimeout(() => {
|
|
321
330
|
if (
|
|
322
331
|
!hasExpired &&
|
|
323
332
|
!this.loanRenewInProgress &&
|
|
324
333
|
!window.IALendingIntervals.tokenPoller
|
|
325
334
|
) {
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
// moment to settle before the first create_token call that follows
|
|
333
|
-
// a real recovery — same underlying datanode-propagation concern
|
|
334
|
-
// the existing 50-second buffer in loanRenewAttempt() guards
|
|
335
|
-
// against for the routine renewal path.
|
|
336
|
-
const tokenPollerDelay = this.recoveringFromLoanExpiry
|
|
337
|
-
? this.tokenPollerRenewalDelay
|
|
338
|
-
: 0;
|
|
335
|
+
// Any confirmed renewal's response already minted a valid access
|
|
336
|
+
// token (see handleLoanAutoRenewed) — the poller only needs to
|
|
337
|
+
// start its recurring check, not fire an immediate confirming
|
|
338
|
+
// create_token call too.
|
|
339
|
+
const skipInitialCall = this.skipNextInitialTokenCall;
|
|
340
|
+
this.skipNextInitialTokenCall = false;
|
|
339
341
|
this.recoveringFromLoanExpiry = false;
|
|
340
|
-
|
|
342
|
+
this.startLoanTokenPoller(skipInitialCall);
|
|
341
343
|
}
|
|
342
344
|
}, 100);
|
|
345
|
+
}
|
|
343
346
|
|
|
344
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Is this BookReader's own init-time jump to the last-read page, rather
|
|
349
|
+
* than a real patron interaction (which fires the identical event)? Only
|
|
350
|
+
* an explicit `false` counts; a missing flag must be treated as real.
|
|
351
|
+
* @param {Event} event - the BookReader:userAction event
|
|
352
|
+
* @returns {boolean}
|
|
353
|
+
*/
|
|
354
|
+
isBookReaderInitAction(event) {
|
|
355
|
+
return event?.detail?.props?.init?.initComplete === false;
|
|
345
356
|
}
|
|
346
357
|
|
|
347
358
|
/**
|
|
@@ -354,15 +365,17 @@ export default class IABookActions extends LitElement {
|
|
|
354
365
|
/**
|
|
355
366
|
* dispatched this event from bookreader page changed
|
|
356
367
|
*/
|
|
357
|
-
window.addEventListener('BookReader:userAction',
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
//
|
|
368
|
+
window.addEventListener('BookReader:userAction', event => {
|
|
369
|
+
if (this.isBookReaderInitAction(event)) {
|
|
370
|
+
log(
|
|
371
|
+
'[IABookActions] BookReader:userAction ignored — fired by BookReader init'
|
|
372
|
+
);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Capture before autoRenewExpiredLoan() runs — it optimistically
|
|
377
|
+
// flips browsingExpired to false, which would otherwise also let
|
|
378
|
+
// autoLoanRenewChecker() run and clobber the in-flight renewal.
|
|
366
379
|
const wasExpired = this.lendingStatus.browsingExpired;
|
|
367
380
|
|
|
368
381
|
log('[IABookActions] BookReader:userAction received', {
|
|
@@ -370,7 +383,6 @@ export default class IABookActions extends LitElement {
|
|
|
370
383
|
browsingExpired: wasExpired,
|
|
371
384
|
});
|
|
372
385
|
|
|
373
|
-
// If the loan expired while the tab stayed visible, auto-renew on page turn
|
|
374
386
|
if (wasExpired) {
|
|
375
387
|
this.autoRenewExpiredLoan();
|
|
376
388
|
}
|
|
@@ -380,18 +392,18 @@ export default class IABookActions extends LitElement {
|
|
|
380
392
|
}
|
|
381
393
|
});
|
|
382
394
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
* so when user open that tab again,
|
|
386
|
-
* the [visibilitychange] event can trigger timer to show relavent messages
|
|
387
|
-
*/
|
|
395
|
+
// A tab in the background can have its intervals throttled/paused, so
|
|
396
|
+
// re-check status when the patron comes back to it.
|
|
388
397
|
document.addEventListener('visibilitychange', async () => {
|
|
389
398
|
if (document.hidden) {
|
|
390
399
|
log('[IABookActions] visibilitychange: tab backgrounded');
|
|
391
400
|
return;
|
|
392
401
|
}
|
|
393
402
|
|
|
394
|
-
log(
|
|
403
|
+
log(
|
|
404
|
+
'[IABookActions] visibilitychange: tab foregrounded',
|
|
405
|
+
this.borrowType
|
|
406
|
+
);
|
|
395
407
|
|
|
396
408
|
try {
|
|
397
409
|
// Loan already expired while tab was hidden — try to silently renew
|
|
@@ -413,9 +425,11 @@ export default class IABookActions extends LitElement {
|
|
|
413
425
|
if (secondsLeft >= this.timerExecutionSeconds) {
|
|
414
426
|
this.loanStatusCheckInterval(Number(secondsLeft));
|
|
415
427
|
} else {
|
|
416
|
-
// Loan expired while
|
|
428
|
+
// Loan expired while away — silently renew. Only clear
|
|
429
|
+
// intervals; disconnectedCallback() would drop the resize
|
|
430
|
+
// observer for a session that's still continuing.
|
|
431
|
+
window?.IALendingIntervals?.clearAll();
|
|
417
432
|
this.autoRenewExpiredLoan();
|
|
418
|
-
this.disconnectedCallback();
|
|
419
433
|
}
|
|
420
434
|
}
|
|
421
435
|
} catch (error) {
|
|
@@ -433,14 +447,8 @@ export default class IABookActions extends LitElement {
|
|
|
433
447
|
* @param {Boolean} hasPageChanged
|
|
434
448
|
*/
|
|
435
449
|
async autoLoanRenewChecker(hasPageChanged = false) {
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
// (e.g. a single scroll gesture), and this method has no debouncing of
|
|
439
|
-
// its own — without this guard, each event would spin up its own
|
|
440
|
-
// LoanRenewHelper concurrently, and whichever's async localCache reads
|
|
441
|
-
// resolve last would clobber this.loanRenewResult, potentially
|
|
442
|
-
// re-triggering the whole renew_loan/create_token flow while a
|
|
443
|
-
// renewal from an earlier event is already in flight or just landed.
|
|
450
|
+
// Re-entrancy guard against rapid BookReader:userAction events (e.g. a
|
|
451
|
+
// single scroll firing several in quick succession).
|
|
444
452
|
if (this.loanRenewInProgress) return;
|
|
445
453
|
|
|
446
454
|
this.loanRenewHelper = new LoanRenewHelper(
|
|
@@ -455,28 +463,31 @@ export default class IABookActions extends LitElement {
|
|
|
455
463
|
}
|
|
456
464
|
|
|
457
465
|
/**
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
* Reuses the same renew_loan path as the automatic loan-renew checker.
|
|
463
|
-
* On success: handleLoanAutoRenewed() resets the timer seamlessly.
|
|
464
|
-
* On error: handleLendingActionError() shows showLoanUnavailableModal().
|
|
466
|
+
* Silently attempt to renew a browse loan that has expired, on
|
|
467
|
+
* visibilitychange or a page turn. handleLoanAutoRenewed()/
|
|
468
|
+
* handleLendingActionError() handle the outcome either way.
|
|
465
469
|
*/
|
|
466
470
|
autoRenewExpiredLoan() {
|
|
467
|
-
if (this.loanRenewInProgress)
|
|
471
|
+
if (this.loanRenewInProgress) {
|
|
472
|
+
log(
|
|
473
|
+
'[IABookActions] autoRenewExpiredLoan: skipped, renewal already in progress',
|
|
474
|
+
{ identifier: this.identifier }
|
|
475
|
+
);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
log('[IABookActions] autoRenewExpiredLoan: starting silent renewal', {
|
|
479
|
+
identifier: this.identifier,
|
|
480
|
+
});
|
|
468
481
|
this.loanRenewInProgress = true;
|
|
469
482
|
this.recoveringFromLoanExpiry = true;
|
|
470
483
|
|
|
471
484
|
this.modal?.closeModal();
|
|
472
485
|
|
|
473
|
-
// Optimistically flip browsingExpired
|
|
474
|
-
//
|
|
475
|
-
// flight, instead of showing the blue "Borrow" state.
|
|
486
|
+
// Optimistically flip browsingExpired so the bar stays in the reading
|
|
487
|
+
// state during the renew_loan round-trip, instead of showing "Borrow".
|
|
476
488
|
this.lendingStatus = {
|
|
477
489
|
...this.lendingStatus,
|
|
478
490
|
browsingExpired: false,
|
|
479
|
-
secondsLeftOnLoan: this.loanRenewTimeConfig.loanTotalTime,
|
|
480
491
|
};
|
|
481
492
|
|
|
482
493
|
// Defer renewNow until after lendingStatus timer setup completes.
|
|
@@ -507,7 +518,6 @@ export default class IABookActions extends LitElement {
|
|
|
507
518
|
|
|
508
519
|
log('[IABookActions] showWarningModal');
|
|
509
520
|
|
|
510
|
-
// Capture texts and secondsLeft before resetting loanRenewResult
|
|
511
521
|
const {
|
|
512
522
|
texts: warningTexts,
|
|
513
523
|
secondsLeft: rawSecondsLeft,
|
|
@@ -519,7 +529,6 @@ export default class IABookActions extends LitElement {
|
|
|
519
529
|
secondsLeft = secondsLeft > 60 ? secondsLeft : 60;
|
|
520
530
|
}
|
|
521
531
|
|
|
522
|
-
// clear modal and reset renew state
|
|
523
532
|
this.modal.customModalContent = nothing;
|
|
524
533
|
this.modal?.closeModal();
|
|
525
534
|
this.loanRenewResult = { texts: '', renewNow: false };
|
|
@@ -529,19 +538,14 @@ export default class IABookActions extends LitElement {
|
|
|
529
538
|
headerColor: '#194880',
|
|
530
539
|
showCloseButton: false,
|
|
531
540
|
closeOnBackdropClick: false,
|
|
532
|
-
message: html`<span
|
|
533
|
-
|
|
534
|
-
>
|
|
535
|
-
<span>${this.loanRenewHelper?.getMessageTexts(
|
|
536
|
-
warningTexts,
|
|
537
|
-
secondsLeft
|
|
538
|
-
)}</span>
|
|
541
|
+
message: html`<span>
|
|
542
|
+
${this.loanRenewHelper?.getMessageTexts(warningTexts, secondsLeft)}
|
|
539
543
|
<a
|
|
540
544
|
href="https://help.archive.org/help/borrowing-from-the-lending-library"
|
|
541
545
|
target="_blank"
|
|
542
546
|
title="Get more info on borrowing from The Lending Library"
|
|
543
547
|
data-event-click-tracking="BookReader|BrowsableMoreInfo"
|
|
544
|
-
style="display:inline-
|
|
548
|
+
style="display:inline-block;vertical-align:middle;line-height:0;margin-left:4px;"
|
|
545
549
|
>
|
|
546
550
|
${infoIcon}
|
|
547
551
|
</a>
|
|
@@ -559,7 +563,8 @@ export default class IABookActions extends LitElement {
|
|
|
559
563
|
>
|
|
560
564
|
Okay
|
|
561
565
|
</button>
|
|
562
|
-
</div>
|
|
566
|
+
</div>
|
|
567
|
+
`;
|
|
563
568
|
|
|
564
569
|
this.modal.setAttribute('aria-live', 'assertive');
|
|
565
570
|
await this.modal?.showModal({ config, customModalContent });
|
|
@@ -576,7 +581,10 @@ export default class IABookActions extends LitElement {
|
|
|
576
581
|
* Execute when loan is expired
|
|
577
582
|
*/
|
|
578
583
|
async browseHasExpired() {
|
|
579
|
-
log('[IABookActions] browseHasExpired'
|
|
584
|
+
log('[IABookActions] browseHasExpired', {
|
|
585
|
+
identifier: this.identifier,
|
|
586
|
+
loanRenewInProgress: this.loanRenewInProgress,
|
|
587
|
+
});
|
|
580
588
|
window?.IALendingIntervals?.clearAll();
|
|
581
589
|
|
|
582
590
|
const currStatus = {
|
|
@@ -589,6 +597,12 @@ export default class IABookActions extends LitElement {
|
|
|
589
597
|
// remove respected key:value for loan-renew
|
|
590
598
|
await this.localCache.delete(`${this.identifier}-loanTime`);
|
|
591
599
|
await this.localCache.delete(`${this.identifier}-pageChangedTime`);
|
|
600
|
+
log(
|
|
601
|
+
'[IABookActions] browseHasExpired: cleared loanTime/pageChangedTime cache',
|
|
602
|
+
{
|
|
603
|
+
identifier: this.identifier,
|
|
604
|
+
}
|
|
605
|
+
);
|
|
592
606
|
|
|
593
607
|
// show message after browsed book is expired.
|
|
594
608
|
this.loanRenewResult.renewNow = false;
|
|
@@ -596,6 +610,9 @@ export default class IABookActions extends LitElement {
|
|
|
596
610
|
'This book has been returned due to inactivity.';
|
|
597
611
|
|
|
598
612
|
this.modal?.closeModal();
|
|
613
|
+
// Release the warning-modal re-entrancy guard, or it stays latched for
|
|
614
|
+
// the life of the component.
|
|
615
|
+
this.warningModalOpen = false;
|
|
599
616
|
|
|
600
617
|
this.sentryCaptureMsg(sentryLogs.browseHasExpired);
|
|
601
618
|
}
|
|
@@ -702,23 +719,31 @@ export default class IABookActions extends LitElement {
|
|
|
702
719
|
}
|
|
703
720
|
|
|
704
721
|
/**
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
* - then change the remaining time
|
|
708
|
-
* - reset timer state
|
|
722
|
+
* Runs after a loan renewal completes (successfully or not): shows the
|
|
723
|
+
* outcome, updates the remaining time, and resets renewal-in-flight state.
|
|
709
724
|
* @param {object} event
|
|
710
725
|
*/
|
|
711
726
|
async handleLoanAutoRenewed({ detail }) {
|
|
712
727
|
const activeLoan = detail?.data?.loan;
|
|
713
|
-
|
|
714
|
-
|
|
728
|
+
|
|
729
|
+
// Treat anything but a confirmed renewal as a failure, or
|
|
730
|
+
// loanRenewInProgress stays latched forever (autoLoanRenewChecker/
|
|
731
|
+
// autoRenewExpiredLoan both no-op on it).
|
|
732
|
+
if (!activeLoan?.renewal) {
|
|
733
|
+
this.loanRenewInProgress = false;
|
|
715
734
|
this.recoveringFromLoanExpiry = false;
|
|
716
|
-
this.
|
|
735
|
+
this.warningModalOpen = false;
|
|
736
|
+
|
|
737
|
+
// dispatchActionError() has already reported this and shown the
|
|
738
|
+
// relevant modal; don't stack a second one on top.
|
|
739
|
+
log('[IABookActions] handleLoanAutoRenewed: not a confirmed renewal', {
|
|
740
|
+
identifier: this.identifier,
|
|
741
|
+
activeLoan,
|
|
742
|
+
});
|
|
717
743
|
return;
|
|
718
744
|
}
|
|
719
745
|
|
|
720
746
|
if (this.loanRenewResult.renewNow) {
|
|
721
|
-
// Now, let's reset loan duration & this.lendingStatus
|
|
722
747
|
const loanTime = await this.localCache.get(`${this.identifier}-loanTime`);
|
|
723
748
|
|
|
724
749
|
// number of seconds left in current loan
|
|
@@ -735,18 +760,28 @@ export default class IABookActions extends LitElement {
|
|
|
735
760
|
ajaxResponse: detail?.data,
|
|
736
761
|
});
|
|
737
762
|
|
|
763
|
+
// BookLoanService::attempt_to_renew_loan() mints a valid access
|
|
764
|
+
// token as part of EVERY renew_loan response, not just a recovery
|
|
765
|
+
// from expiry — so the poller restart below never needs to fire a
|
|
766
|
+
// redundant confirming create_token call, regardless of which path
|
|
767
|
+
// triggered this renewal.
|
|
768
|
+
this.skipNextInitialTokenCall = true;
|
|
769
|
+
|
|
770
|
+
// A page image requested in the gap between expiry and this renewal
|
|
771
|
+
// landing can come back broken — browsers don't retry a failed <img>
|
|
772
|
+
// on their own. Retry once, right when access is confirmed good
|
|
773
|
+
// again, rather than leaving it broken until the patron reloads.
|
|
774
|
+
this.reloadPageImages();
|
|
775
|
+
|
|
738
776
|
if (this.recoveringFromLoanExpiry) {
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
// not here — it decides whether that restart needs a datanode
|
|
748
|
-
// propagation buffer (see setupLendingToolbarActions).
|
|
749
|
-
this.postInitComplete = false;
|
|
777
|
+
// Only now is it safe to re-initialize BookReader — not for a
|
|
778
|
+
// routine top-up, where that would be disruptive. Call it
|
|
779
|
+
// directly rather than waiting on a confirming create_token
|
|
780
|
+
// success: BookLoanService::attempt_to_renew_loan() already
|
|
781
|
+
// minted a valid access token as part of this renew_loan
|
|
782
|
+
// response, so there's nothing left to confirm.
|
|
783
|
+
this.lendingBarPostInit();
|
|
784
|
+
this.postInitComplete = true;
|
|
750
785
|
}
|
|
751
786
|
|
|
752
787
|
const currStatus = {
|
|
@@ -769,67 +804,55 @@ export default class IABookActions extends LitElement {
|
|
|
769
804
|
this.loanRenewInProgress = false;
|
|
770
805
|
}
|
|
771
806
|
|
|
772
|
-
/**
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
* @memberof IABookActions
|
|
776
|
-
*/
|
|
807
|
+
/** Start the countdown interval; ticks read the live secondsLeftOnLoan
|
|
808
|
+
* each time rather than a value frozen at start, so drift can't compound
|
|
809
|
+
* across ticks (see loanStatusCheckInterval). */
|
|
777
810
|
async startTimerCountdown() {
|
|
778
811
|
window?.IALendingIntervals?.clearTimerCountdown();
|
|
779
|
-
|
|
780
|
-
const secondsLeft = Number(this.lendingStatus.secondsLeftOnLoan);
|
|
781
812
|
this.timeWhenTimerStart = new Date();
|
|
782
813
|
|
|
783
814
|
window.IALendingIntervals.timerCountdown = setInterval(async () => {
|
|
784
|
-
|
|
785
|
-
|
|
815
|
+
await this.loanStatusCheckInterval(
|
|
816
|
+
Number(this.lendingStatus.secondsLeftOnLoan)
|
|
817
|
+
);
|
|
786
818
|
}, this.timerExecutionSeconds * 1000);
|
|
787
819
|
}
|
|
788
820
|
|
|
789
821
|
/**
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
* - loan-renew attempt to renew the loan OR show relavant message
|
|
793
|
-
* - if loan has expired, clear interval timers
|
|
794
|
-
*
|
|
822
|
+
* Runs on every countdown tick: resyncs against wall-clock time, attempts
|
|
823
|
+
* a renewal near expiry, and clears the timers once the loan expires.
|
|
795
824
|
* @param {Number} secondsLeftOnLoan
|
|
796
825
|
*/
|
|
797
826
|
async loanStatusCheckInterval(secondsLeftOnLoan) {
|
|
798
827
|
let secondsLeft = secondsLeftOnLoan;
|
|
799
828
|
secondsLeft -= this.timerExecutionSeconds;
|
|
800
|
-
secondsLeft = Math.round(secondsLeft);
|
|
829
|
+
secondsLeft = Math.round(secondsLeft);
|
|
801
830
|
|
|
802
|
-
// re-sync timer if gone off because of background window
|
|
803
|
-
// side effect: updates this.lendingStatus & kicks off lifecycle,
|
|
804
|
-
// if really updated, escape from here
|
|
805
831
|
const resyncd = this.reSyncTimerIfGoneOff(secondsLeft);
|
|
806
|
-
|
|
807
832
|
if (resyncd.hasSynced) {
|
|
808
833
|
secondsLeft = resyncd.whatShouldLeft;
|
|
809
|
-
log('[IABookActions] timer: timer re-synced', {
|
|
810
|
-
secondsLeft,
|
|
811
|
-
});
|
|
834
|
+
log('[IABookActions] timer: timer re-synced', { secondsLeft });
|
|
812
835
|
}
|
|
813
836
|
|
|
814
837
|
log('[IABookActions] timer', {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
pageChangedInLast: this.loanRenewTimeConfig.pageChangedInLast,
|
|
838
|
+
whatShouldLeft: resyncd.whatShouldLeft,
|
|
839
|
+
whatIsleft: secondsLeft,
|
|
818
840
|
});
|
|
819
841
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
842
|
+
// Re-anchor every tick so drift from wall-clock time can't compound
|
|
843
|
+
// across ticks that don't happen to trigger a resync above.
|
|
844
|
+
this.timeWhenTimerStart = new Date();
|
|
845
|
+
this.lendingStatus = { ...this.lendingStatus, secondsLeftOnLoan: secondsLeft };
|
|
846
|
+
|
|
847
|
+
// 10 minutes out: start checking for a renewal. 0: show the "about to
|
|
848
|
+
// auto-return" warning. @see IABookActions::bindLoanRenewEvents
|
|
826
849
|
if (secondsLeft <= this.loanRenewTimeConfig.loanRenewAtLast) {
|
|
827
850
|
await this.loanRenewAttempt(secondsLeft);
|
|
828
851
|
}
|
|
829
852
|
|
|
830
|
-
// clear interval in secondsLeft if less
|
|
831
853
|
if (secondsLeft <= this.timerExecutionSeconds) {
|
|
832
|
-
|
|
854
|
+
window?.IALendingIntervals?.clearAll();
|
|
855
|
+
this.tokenPoller?.disconnectedCallback();
|
|
833
856
|
this.sentryCaptureMsg(sentryLogs.clearOneHourTimer);
|
|
834
857
|
}
|
|
835
858
|
}
|
|
@@ -843,6 +866,15 @@ export default class IABookActions extends LitElement {
|
|
|
843
866
|
reSyncTimerIfGoneOff(timerSecondsLeft) {
|
|
844
867
|
const currentTime = new Date();
|
|
845
868
|
|
|
869
|
+
// No prior anchor to compare against -- e.g. visibilitychange calling
|
|
870
|
+
// loanStatusCheckInterval() directly before startTimerCountdown() ever
|
|
871
|
+
// ran, which setupLendingToolbarActions() now skips while a renewal is
|
|
872
|
+
// in flight. Nothing to resync yet; anchor now and report no drift.
|
|
873
|
+
if (!this.timeWhenTimerStart) {
|
|
874
|
+
this.timeWhenTimerStart = currentTime;
|
|
875
|
+
return { hasSynced: false, whatShouldLeft: Math.round(timerSecondsLeft) };
|
|
876
|
+
}
|
|
877
|
+
|
|
846
878
|
// current time - loan time
|
|
847
879
|
const diffInSeconds =
|
|
848
880
|
currentTime.getTime() / 1000 - this.timeWhenTimerStart.getTime() / 1000;
|
|
@@ -884,13 +916,8 @@ export default class IABookActions extends LitElement {
|
|
|
884
916
|
*/
|
|
885
917
|
async loanRenewAttempt(secondsLeft) {
|
|
886
918
|
let loanSecondsLeft = secondsLeft;
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
* 1. less time to execute ajax call
|
|
890
|
-
* 2. less time to write loan on datanodes
|
|
891
|
-
* 3. less time to load images by create_token api
|
|
892
|
-
* so if seconds left is < 50, just expire the loan
|
|
893
|
-
*/
|
|
919
|
+
// Under 50s left there isn't enough time for the renew_loan round-trip
|
|
920
|
+
// and create_token to load images, so just expire the loan.
|
|
894
921
|
if (loanSecondsLeft < 50) {
|
|
895
922
|
log('[IABookActions] loanRenewAttempt: < 50s left, expiring loan');
|
|
896
923
|
await this.browseHasExpired();
|
|
@@ -899,14 +926,13 @@ export default class IABookActions extends LitElement {
|
|
|
899
926
|
|
|
900
927
|
await this.autoLoanRenewChecker(false);
|
|
901
928
|
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
*/
|
|
929
|
+
// Once dismissed, don't re-show the warning on every subsequent tick —
|
|
930
|
+
// only an actual renewal (handleLoanAutoRenewed) re-arms it.
|
|
931
|
+
if (
|
|
932
|
+
this.loanRenewResult.renewNow === false &&
|
|
933
|
+
!this.warningModalDismissed
|
|
934
|
+
) {
|
|
935
|
+
// Compensate for the 50s buffer above by warning a minute early.
|
|
910
936
|
loanSecondsLeft -= 60;
|
|
911
937
|
this.loanRenewResult.secondsLeft = loanSecondsLeft;
|
|
912
938
|
|
|
@@ -916,9 +942,10 @@ export default class IABookActions extends LitElement {
|
|
|
916
942
|
|
|
917
943
|
/**
|
|
918
944
|
* enable access of borrowed/browsed books
|
|
945
|
+
* @param {boolean} [skipInitialCall] - see LoanTokenPoller's constructor doc
|
|
919
946
|
* @see LoanTokenPoller
|
|
920
947
|
*/
|
|
921
|
-
startLoanTokenPoller() {
|
|
948
|
+
startLoanTokenPoller(skipInitialCall = false) {
|
|
922
949
|
const successCallback = () => {
|
|
923
950
|
if (!this.postInitComplete) {
|
|
924
951
|
this.lendingBarPostInit();
|
|
@@ -929,24 +956,17 @@ export default class IABookActions extends LitElement {
|
|
|
929
956
|
this.handleLendingActionError(eventObj);
|
|
930
957
|
};
|
|
931
958
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
* - disptach lendingFlow::PostInit event
|
|
939
|
-
* - initialize bookreader using br.init()
|
|
940
|
-
* 4. errorCallback
|
|
941
|
-
* 5. tokenPollerDelay
|
|
942
|
-
*/
|
|
943
|
-
this.tokenPoller = new LoanTokenPoller(
|
|
944
|
-
this.identifier,
|
|
945
|
-
this.borrowType,
|
|
959
|
+
// Tear down any previous poller first, so only one is ever controlling
|
|
960
|
+
// window.IALendingIntervals.tokenPoller at a time.
|
|
961
|
+
this.tokenPoller?.disconnectedCallback();
|
|
962
|
+
this.tokenPoller = new LoanTokenPoller({
|
|
963
|
+
identifier: this.identifier,
|
|
964
|
+
borrowType: this.borrowType,
|
|
946
965
|
successCallback,
|
|
947
966
|
errorCallback,
|
|
948
|
-
this.tokenDelay // in seconds
|
|
949
|
-
|
|
967
|
+
pollerDelay: this.tokenDelay, // in seconds
|
|
968
|
+
skipInitialCall,
|
|
969
|
+
});
|
|
950
970
|
}
|
|
951
971
|
|
|
952
972
|
/*
|
|
@@ -959,45 +979,63 @@ export default class IABookActions extends LitElement {
|
|
|
959
979
|
}
|
|
960
980
|
|
|
961
981
|
/**
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
982
|
+
* Handles lending errors from any action (browse_book, borrow_book,
|
|
983
|
+
* create_token, renew_loan, etc).
|
|
965
984
|
* @event IABookActions#lendingActionError
|
|
966
|
-
* @param {Object} event
|
|
967
|
-
*
|
|
968
|
-
*
|
|
985
|
+
* @param {Object} event
|
|
986
|
+
* @param {string} event.detail.action
|
|
987
|
+
* @param {string} event.detail.data.error
|
|
969
988
|
*/
|
|
970
989
|
handleLendingActionError(event) {
|
|
971
990
|
this.disableActionGroup = false;
|
|
972
991
|
|
|
973
992
|
const action = event?.detail?.action;
|
|
974
|
-
|
|
993
|
+
// handleLoanRenewNow's failure path sets `.error` to a boolean flag and
|
|
994
|
+
// puts the real text in `.message` — only take `.error` when it's
|
|
995
|
+
// actually a string, or the modal renders the literal word "true".
|
|
996
|
+
const rawErrorMsg = event?.detail?.data?.error;
|
|
997
|
+
const errorMsg =
|
|
998
|
+
typeof rawErrorMsg === 'string' ? rawErrorMsg : undefined;
|
|
999
|
+
const isInitial = event?.detail?.isInitial === true;
|
|
1000
|
+
|
|
1001
|
+
log('[IABookActions] handleLendingActionError', {
|
|
1002
|
+
identifier: this.identifier,
|
|
1003
|
+
action,
|
|
1004
|
+
errorMsg,
|
|
1005
|
+
isInitial,
|
|
1006
|
+
loanRenewInProgress: this.loanRenewInProgress,
|
|
1007
|
+
recoveringFromLoanExpiry: this.recoveringFromLoanExpiry,
|
|
1008
|
+
});
|
|
975
1009
|
|
|
976
1010
|
if (action === 'create_token') {
|
|
977
|
-
//
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
//
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
|
|
1011
|
+
// Any create_token failure — initial or a routine interval refresh —
|
|
1012
|
+
// means access can no longer be confirmed (e.g. a stale loan-record
|
|
1013
|
+
// cache on some other node can make this request fail even though
|
|
1014
|
+
// the loan is fine, or the loan may genuinely have just been
|
|
1015
|
+
// returned via BookReaderImages.php's revokeAccess()). Don't retry
|
|
1016
|
+
// and don't stay silent either way: reset the bar to Borrow and the
|
|
1017
|
+
// timer to 0 rather than leaving a stale countdown for a session
|
|
1018
|
+
// that isn't accessible.
|
|
1019
|
+
window?.IALendingIntervals?.clearAll();
|
|
1020
|
+
this.tokenPoller?.disconnectedCallback();
|
|
986
1021
|
this.lendingStatus = {
|
|
987
1022
|
...this.lendingStatus,
|
|
988
1023
|
user_has_browsed: false,
|
|
989
1024
|
available_to_browse: true,
|
|
1025
|
+
secondsLeftOnLoan: 0,
|
|
990
1026
|
};
|
|
1027
|
+
|
|
1028
|
+
// showErrorModal has dedicated create_token messaging (refresh
|
|
1029
|
+
// button + support email) and shows regardless of whether a
|
|
1030
|
+
// specific error string came back.
|
|
1031
|
+
this.showErrorModal(errorMsg, action);
|
|
991
1032
|
} else if (action === 'renew_loan') {
|
|
992
1033
|
window?.IALendingIntervals?.clearAll();
|
|
993
1034
|
this.loanRenewInProgress = false;
|
|
994
1035
|
this.recoveringFromLoanExpiry = false;
|
|
995
1036
|
|
|
996
|
-
// The loan was NOT
|
|
997
|
-
//
|
|
998
|
-
// the action bar showing "Return now" with a frozen countdown from
|
|
999
|
-
// before the failed attempt, which falsely implies the book is
|
|
1000
|
-
// still actively being read.
|
|
1037
|
+
// The loan was NOT renewed — reflect that immediately (show Borrow,
|
|
1038
|
+
// clear the stale timer) rather than leaving a frozen "Return now".
|
|
1001
1039
|
this.lendingStatus = {
|
|
1002
1040
|
...this.lendingStatus,
|
|
1003
1041
|
user_has_browsed: false,
|
|
@@ -1005,11 +1043,8 @@ export default class IABookActions extends LitElement {
|
|
|
1005
1043
|
secondsLeftOnLoan: 0,
|
|
1006
1044
|
};
|
|
1007
1045
|
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
// now authoritatively considers true — matching on "not available"
|
|
1011
|
-
// to guess the reason was fragile since the server can return any
|
|
1012
|
-
// message (e.g. a lending limit).
|
|
1046
|
+
// Refresh the page on dismissal so the client picks up whatever the
|
|
1047
|
+
// server now authoritatively considers true (any error message).
|
|
1013
1048
|
this.showLoanUnavailableModal(errorMsg);
|
|
1014
1049
|
} else {
|
|
1015
1050
|
// Every other action failure genuinely affects loan state.
|
|
@@ -1022,9 +1057,15 @@ export default class IABookActions extends LitElement {
|
|
|
1022
1057
|
// update action bar state if book is not available to browse or borrow.
|
|
1023
1058
|
if (errorMsg.match(/not available to borrow/gm)) {
|
|
1024
1059
|
if (action === 'browse_book') {
|
|
1025
|
-
this.lendingStatus = {
|
|
1060
|
+
this.lendingStatus = {
|
|
1061
|
+
...this.lendingStatus,
|
|
1062
|
+
available_to_browse: false,
|
|
1063
|
+
};
|
|
1026
1064
|
} else if (action === 'borrow_book') {
|
|
1027
|
-
this.lendingStatus = {
|
|
1065
|
+
this.lendingStatus = {
|
|
1066
|
+
...this.lendingStatus,
|
|
1067
|
+
available_to_borrow: false,
|
|
1068
|
+
};
|
|
1028
1069
|
}
|
|
1029
1070
|
}
|
|
1030
1071
|
}
|
|
@@ -1055,7 +1096,7 @@ export default class IABookActions extends LitElement {
|
|
|
1055
1096
|
.identifier}"
|
|
1056
1097
|
>info@archive.org</a
|
|
1057
1098
|
><br /><br />
|
|
1058
|
-
|
|
1099
|
+
${errorMsg ? html`<code>errorLog: ${errorMsg}</code>` : nothing}`;
|
|
1059
1100
|
}
|
|
1060
1101
|
|
|
1061
1102
|
await this.modal?.showModal({
|
|
@@ -1109,6 +1150,10 @@ export default class IABookActions extends LitElement {
|
|
|
1109
1150
|
justify-content: center;
|
|
1110
1151
|
flex-wrap: wrap;
|
|
1111
1152
|
}
|
|
1153
|
+
|
|
1154
|
+
#action-bar-modal {
|
|
1155
|
+
--modalWidth: 36rem;
|
|
1156
|
+
}
|
|
1112
1157
|
`;
|
|
1113
1158
|
}
|
|
1114
1159
|
}
|